From 391a3eb36f72ff8c6225aa41dc7b85be58cd3315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaakko=20Kera=CC=88nen?= Date: Sun, 18 Jun 2017 13:19:37 +0300 Subject: [PATCH] Scripting|Model Renderer|libcore: Script callbacks for model state change Added de::ConditionalTrigger for issuing a callback under the specified circumstances. Configured with a Variable. Model assets can use `notifiedStates` and `onStateChange()` to observe when the mobj changes to (a) specific state(s). Value::is<>() was removed, now should only use de::is<>. Renamed de::Scheduler to de::Timeline. --- doomsday/apps/client/include/render/model.h | 6 +- .../client/include/render/modelrenderer.h | 2 +- .../client/include/render/stateanimator.h | 3 +- doomsday/apps/client/src/configprofiles.cpp | 6 +- .../client/src/network/base/masterserver.cpp | 2 +- .../apps/client/src/network/serverlink.cpp | 2 +- doomsday/apps/client/src/render/model.cpp | 2 +- .../apps/client/src/render/modelloader.cpp | 2 +- .../src/render/playerweaponanimator.cpp | 2 +- .../apps/client/src/render/stateanimator.cpp | 82 ++++++++++--- .../src/ui/editors/modelasseteditor.cpp | 6 +- .../apps/libdoomsday/src/defs/dedregister.cpp | 2 +- .../apps/libdoomsday/src/gamestatefolder.cpp | 2 +- doomsday/apps/libdoomsday/src/urivalue.cpp | 2 +- .../src/widgets/scriptcommandwidget.cpp | 2 +- .../src/widgets/variablechoicewidget.cpp | 4 +- .../src/widgets/variablesliderwidget.cpp | 2 +- .../sdk/libcore/include/de/ConditionalTrigger | 1 + doomsday/sdk/libcore/include/de/Timeline | 1 + .../include/de/data/conditionaltrigger.h | 70 ++++++++++++ doomsday/sdk/libcore/include/de/data/value.h | 31 +---- .../de/scriptsys/{scheduler.h => timeline.h} | 31 ++--- .../libcore/src/data/conditionaltrigger.cpp | 108 ++++++++++++++++++ doomsday/sdk/libcore/src/data/json.cpp | 2 +- doomsday/sdk/libcore/src/data/record.cpp | 2 +- .../sdk/libcore/src/data/recordaccessor.cpp | 16 +-- doomsday/sdk/libcore/src/data/textvalue.cpp | 2 +- doomsday/sdk/libcore/src/data/value.cpp | 17 +++ .../sdk/libcore/src/scriptsys/process.cpp | 2 +- .../scriptsys/{scheduler.cpp => timeline.cpp} | 32 +++--- 30 files changed, 330 insertions(+), 114 deletions(-) create mode 100644 doomsday/sdk/libcore/include/de/ConditionalTrigger create mode 100644 doomsday/sdk/libcore/include/de/Timeline create mode 100644 doomsday/sdk/libcore/include/de/data/conditionaltrigger.h rename doomsday/sdk/libcore/include/de/scriptsys/{scheduler.h => timeline.h} (77%) create mode 100644 doomsday/sdk/libcore/src/data/conditionaltrigger.cpp rename doomsday/sdk/libcore/src/scriptsys/{scheduler.cpp => timeline.cpp} (84%) diff --git a/doomsday/apps/client/include/render/model.h b/doomsday/apps/client/include/render/model.h index 611b74371c..b70c355a80 100644 --- a/doomsday/apps/client/include/render/model.h +++ b/doomsday/apps/client/include/render/model.h @@ -21,7 +21,7 @@ #include #include -#include +#include #include #include @@ -50,7 +50,7 @@ struct Model : public de::ModelDrawable { de::String name; ///< Name of the sequence. de::Record const *def; ///< Record describing the sequence (in asset metadata). - de::Scheduler *timeline = nullptr; ///< Script timeline (owned). + de::Timeline *timeline = nullptr; ///< Script timeline (owned). de::String sharedTimeline; ///< Name of shared timeline (if specified). AnimSequence(de::String const &n, de::Record const &d); @@ -102,7 +102,7 @@ struct Model : public de::ModelDrawable StateAnims animations; /// Shared timelines (not sequence-specific). Owned. - QHash timelines; + QHash timelines; }; Q_DECLARE_OPERATORS_FOR_FLAGS(Model::Flags) diff --git a/doomsday/apps/client/include/render/modelrenderer.h b/doomsday/apps/client/include/render/modelrenderer.h index 28ab2d4520..ec8edf1095 100644 --- a/doomsday/apps/client/include/render/modelrenderer.h +++ b/doomsday/apps/client/include/render/modelrenderer.h @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include #include diff --git a/doomsday/apps/client/include/render/stateanimator.h b/doomsday/apps/client/include/render/stateanimator.h index 3d0cfcd5a5..24ce7e5b30 100644 --- a/doomsday/apps/client/include/render/stateanimator.h +++ b/doomsday/apps/client/include/render/stateanimator.h @@ -29,7 +29,8 @@ namespace render { /** - * State-based object animator for `ModelDrawable`s. + * State-based object animator for `ModelDrawable`s. Triggers animation sequences + * based on state changes and damage points received by the owner. * * Used for both mobjs and psprites. The state and movement of the object * determine which animation sequences are started. diff --git a/doomsday/apps/client/src/configprofiles.cpp b/doomsday/apps/client/src/configprofiles.cpp index ff19680a9f..6f7a2def35 100644 --- a/doomsday/apps/client/src/configprofiles.cpp +++ b/doomsday/apps/client/src/configprofiles.cpp @@ -221,11 +221,11 @@ DENG2_PIMPL(ConfigProfiles) DENG2_ASSERT(confDefaults.has(name)); Variable const &var = confDefaults[name]; - if (var.value().is()) + if (is(var.value())) { return var.value().asNumber(); } - else if (var.value().is()) + else if (is(var.value())) { return var.value().asText(); } @@ -270,7 +270,7 @@ DENG2_PIMPL(ConfigProfiles) case ConfigVariable: { Value const &cfgValue = Config::get(st.name).value(); - if (cfgValue.is()) + if (is(cfgValue)) { val = cfgValue.asNumber(); } diff --git a/doomsday/apps/client/src/network/base/masterserver.cpp b/doomsday/apps/client/src/network/base/masterserver.cpp index f4afca14cf..d905b33bbb 100644 --- a/doomsday/apps/client/src/network/base/masterserver.cpp +++ b/doomsday/apps/client/src/network/base/masterserver.cpp @@ -216,7 +216,7 @@ bool MasterWorker::parseResponse(QByteArray const &response) try { std::unique_ptr entryValue(Value::constructFrom(entry)); - if (!entryValue->is()) + if (!is(*entryValue)) { LOG_NET_WARNING("Server information was in unexpected format"); continue; diff --git a/doomsday/apps/client/src/network/serverlink.cpp b/doomsday/apps/client/src/network/serverlink.cpp index baf4863ec5..c994df3060 100644 --- a/doomsday/apps/client/src/network/serverlink.cpp +++ b/doomsday/apps/client/src/network/serverlink.cpp @@ -119,7 +119,7 @@ DENG2_PIMPL(ServerLink) { QVariant const response = parseJSON(String::fromUtf8(reply.mid(5))); std::unique_ptr rec(Value::constructFrom(response.toMap())); - if (!rec->is()) + if (!is(*rec)) { throw Error("ServerLink::handleInfoResponse", "Failed to parse response contents"); } diff --git a/doomsday/apps/client/src/render/model.cpp b/doomsday/apps/client/src/render/model.cpp index d96b6ec95f..84f7b66a0b 100644 --- a/doomsday/apps/client/src/render/model.cpp +++ b/doomsday/apps/client/src/render/model.cpp @@ -31,7 +31,7 @@ Model::AnimSequence::AnimSequence(String const &name, Record const &def) // Parse timeline events. if (def.hasSubrecord(DEF_TIMELINE)) { - timeline = new Scheduler; + timeline = new Timeline; timeline->addFromInfo(def.subrecord(DEF_TIMELINE)); } else if (def.hasMember(DEF_TIMELINE)) diff --git a/doomsday/apps/client/src/render/modelloader.cpp b/doomsday/apps/client/src/render/modelloader.cpp index 6bfe92acee..b64b057baa 100644 --- a/doomsday/apps/client/src/render/modelloader.cpp +++ b/doomsday/apps/client/src/render/modelloader.cpp @@ -508,7 +508,7 @@ DENG2_PIMPL(ModelLoader) auto timelines = ScriptedInfo::subrecordsOfType(DEF_TIMELINE, asset.subrecord(DEF_ANIMATION)); DENG2_FOR_EACH_CONST(Record::Subrecords, timeline, timelines) { - Scheduler *scheduler = new Scheduler; + Timeline *scheduler = new Timeline; scheduler->addFromInfo(*timeline.value()); model.timelines[timeline.key()] = scheduler; } diff --git a/doomsday/apps/client/src/render/playerweaponanimator.cpp b/doomsday/apps/client/src/render/playerweaponanimator.cpp index 8f96438f69..396a08cc06 100644 --- a/doomsday/apps/client/src/render/playerweaponanimator.cpp +++ b/doomsday/apps/client/src/render/playerweaponanimator.cpp @@ -63,7 +63,7 @@ DENG2_PIMPL_NOREF(PlayerWeaponAnimator) auto &model = modelBank().model(identifier); model.audienceForDeletion() += this; animator.reset(new StateAnimator(identifier, model)); - animator->setOwnerNamespace(player->info(), QStringLiteral("PLAYER")); + animator->setOwnerNamespace(player->info(), QStringLiteral("__player__")); } else { diff --git a/doomsday/apps/client/src/render/stateanimator.cpp b/doomsday/apps/client/src/render/stateanimator.cpp index bc3a8b360c..541baa7b5a 100644 --- a/doomsday/apps/client/src/render/stateanimator.cpp +++ b/doomsday/apps/client/src/render/stateanimator.cpp @@ -25,6 +25,7 @@ #include +#include #include #include #include @@ -32,6 +33,8 @@ #include #include +#include + using namespace de; namespace render { @@ -53,6 +56,8 @@ static String const VAR_ASSET ("__asset__"); // runtime reference to static String const VAR_ENABLED ("enabled"); static String const VAR_MATERIAL ("material"); +static String const VAR_NOTIFIED_STATES("notifiedStates"); + static String const PASS_GLOBAL (""); static String const DEFAULT_MATERIAL ("default"); @@ -75,14 +80,14 @@ DENG2_PIMPL(StateAnimator) LoopMode looping = NotLooping; int priority = ANIM_DEFAULT_PRIORITY; - Scheduler const *timeline = nullptr; // owned by ModelRenderer::AnimSequence - std::unique_ptr clock; + Timeline const *timeline = nullptr; // owned by ModelRenderer::AnimSequence + std::unique_ptr clock; TimeDelta overrideDuration = -1.0; Sequence() {} Sequence(int animationId, String const &rootNode, LoopMode looping, int priority, - Scheduler const *timeline = nullptr) + Timeline const *timeline = nullptr) : looping(looping) , priority(priority) , timeline(timeline) @@ -172,15 +177,31 @@ DENG2_PIMPL(StateAnimator) typedef QHash AnimVars; AnimVars animVars; - Impl(Public *i, DotPath const &id) : Base(i) + /// Optional script callback for chosen states. + struct StateCallback : public ConditionalTrigger { - names.add(Record::VAR_NATIVE_SELF).set(new NativePointerValue(thisPublic)).setReadOnly(); - names.addSuperRecord(ScriptSystem::builtInClass(QStringLiteral("Render"), - QStringLiteral("StateAnimator"))); - names.addText(VAR_ID, id).setReadOnly(); - names.add(VAR_ASSET).set(new RecordValue(App::asset(id).accessedRecord())).setReadOnly(); + Record &names; + + StateCallback(Record &names) : names(names) + { + setCondition(names[VAR_NOTIFIED_STATES]); + } - initVariables(); + void handleTriggered(String const &trigger) override + { + Record ns; + ns.add(QStringLiteral("self")).set(new RecordValue(names)); + Process::scriptCall(Process::IgnoreResult, ns, + QStringLiteral("self.__asset__.onStateChange"), + "$self", // StateAnimator instance + trigger); // new state + } + }; + std::unique_ptr stateCallback; + + Impl(Public *i, DotPath const &assetId) : Base(i) + { + initVariables(assetId); // Set up the model drawing parameters. if (!self().model().passes.isEmpty()) @@ -205,8 +226,30 @@ DENG2_PIMPL(StateAnimator) deinitVariables(); } - void initVariables() + void initVariables(DotPath const &assetId) { + // Initialize the StateAnimator script object. + names.add(Record::VAR_NATIVE_SELF).set(new NativePointerValue(thisPublic)).setReadOnly(); + names.addSuperRecord(ScriptSystem::builtInClass(QStringLiteral("Render"), + QStringLiteral("StateAnimator"))); + names.addText(VAR_ID, assetId).setReadOnly(); + Record const &assetDef = App::asset(assetId).accessedRecord(); + names.add(VAR_ASSET).set(new RecordValue(assetDef)).setReadOnly(); + if (assetDef.has(VAR_NOTIFIED_STATES)) + { + // Use the initial value for state callbacks. + names.add(VAR_NOTIFIED_STATES).set(assetDef.get(VAR_NOTIFIED_STATES)); + } + else + { + // The states to notify can be chosen later. + names.addArray(VAR_NOTIFIED_STATES); + } + if (assetDef.has(QStringLiteral("onStateChange"))) + { + stateCallback.reset(new StateCallback(names)); + } + int const passCount = self().model().passes.size(); // Clear lookups affected by the variables. @@ -523,7 +566,7 @@ DENG2_PIMPL(StateAnimator) anim.apply(spec); if (anim.timeline) { - anim.clock.reset(new Scheduler::Clock(*anim.timeline, &names)); + anim.clock.reset(new Timeline::Clock(*anim.timeline, &names)); } applyFlagOperation(anim.flags, Sequence::ClampToDuration, anim.looping == Sequence::Looping? UnsetFlags : SetFlags); @@ -547,12 +590,12 @@ void StateAnimator::setOwnerNamespace(Record &names, String const &varName) d->names.add(d->ownerNamespaceVarName).set(new RecordValue(names)); // Call the onInit() function if there is one. - if (d->names.has(VAR_ASSET + QStringLiteral(".onInit"))) + if (d->names.has(QStringLiteral("__asset__.onInit"))) { Record ns; ns.add(QStringLiteral("self")).set(new RecordValue(d->names)); Process::scriptCall(Process::IgnoreResult, ns, - "self." + VAR_ASSET + ".onInit", + "self.__asset__.onInit", "$self"); } } @@ -566,6 +609,11 @@ void StateAnimator::triggerByState(String const &stateName) { using Sequence = Impl::Sequence; + if (d->stateCallback) + { + d->stateCallback->tryTrigger(stateName); + } + // No animations can be triggered if none are available. auto const *stateAnims = &model().animations; if (!stateAnims) return; @@ -603,7 +651,7 @@ void StateAnimator::triggerByState(String const &stateName) int const priority = seq.def->geti(DEF_PRIORITY, ANIM_DEFAULT_PRIORITY); // Look up the timeline. - Scheduler *timeline = seq.timeline; + Timeline *timeline = seq.timeline; if (!seq.sharedTimeline.isEmpty()) { auto tl = model().timelines.constFind(seq.sharedTimeline); @@ -663,7 +711,7 @@ void StateAnimator::triggerDamage(int points, struct mobj_s const *inflictor) * variable holds a direct pointer to the asset definition, where the * function is defined. */ - if (d->names.has(VAR_ASSET + QStringLiteral(".onDamage"))) + if (d->names.has(QStringLiteral("__asset__.onDamage"))) { /* * We need to provide the StateAnimator instance to the script as an @@ -673,7 +721,7 @@ void StateAnimator::triggerDamage(int points, struct mobj_s const *inflictor) Record ns; ns.add(QStringLiteral("self")).set(new RecordValue(d->names)); Process::scriptCall(Process::IgnoreResult, ns, - "self." + VAR_ASSET + ".onDamage", + QStringLiteral("self.__asset__.onDamage"), "$self", points, inflictor? &THINKER_DATA(inflictor->thinker, ThinkerData) : nullptr); diff --git a/doomsday/apps/client/src/ui/editors/modelasseteditor.cpp b/doomsday/apps/client/src/ui/editors/modelasseteditor.cpp index 2500291c85..f61e3516af 100644 --- a/doomsday/apps/client/src/ui/editors/modelasseteditor.cpp +++ b/doomsday/apps/client/src/ui/editors/modelasseteditor.cpp @@ -391,17 +391,17 @@ DENG_GUI_PIMPL(ModelAssetEditor) g->addSlider(var, range, step, precision); } } - else if (var.value().is()) + else if (is(var.value())) { g->addLabel(varLabel(label)); g->addLineEdit(var); } - else if (var.value().is()) + else if (is(var.value())) { g->addLabel(varLabel(label)); g->addSlider(var, range, step, precision); } - else if (descend && var.value().is()) + else if (descend && is(var.value())) { populateGroup(g, var.valueAsRecord(), descend, label); } diff --git a/doomsday/apps/libdoomsday/src/defs/dedregister.cpp b/doomsday/apps/libdoomsday/src/defs/dedregister.cpp index cac06c0933..5496207958 100644 --- a/doomsday/apps/libdoomsday/src/defs/dedregister.cpp +++ b/doomsday/apps/libdoomsday/src/defs/dedregister.cpp @@ -156,7 +156,7 @@ DENG2_PIMPL(DEDRegister) bool isEmptyKeyValue(Value const &value) const { - return value.is() && value.asText().isEmpty(); + return is(value) && value.asText().isEmpty(); } bool isValidKeyValue(Value const &value) const diff --git a/doomsday/apps/libdoomsday/src/gamestatefolder.cpp b/doomsday/apps/libdoomsday/src/gamestatefolder.cpp index 84929f726a..534dbee81a 100644 --- a/doomsday/apps/libdoomsday/src/gamestatefolder.cpp +++ b/doomsday/apps/libdoomsday/src/gamestatefolder.cpp @@ -455,7 +455,7 @@ String GameStateFolder::Metadata::asTextWithInfoSyntax() const { Value const &value = i.value()->value(); String valueAsText = value.asText(); - if (value.is()) + if (is(value)) { valueAsText = "\"" + valueAsText.replace("\"", "''") + "\""; } diff --git a/doomsday/apps/libdoomsday/src/urivalue.cpp b/doomsday/apps/libdoomsday/src/urivalue.cpp index 7929e56e8a..0f85351034 100644 --- a/doomsday/apps/libdoomsday/src/urivalue.cpp +++ b/doomsday/apps/libdoomsday/src/urivalue.cpp @@ -63,7 +63,7 @@ bool UriValue::contains(Value const &value) const { // We are able to look for substrings within the text, without applying automatic // type conversions. - if (value.is()) + if (is(value)) { return _uri.asText().indexOf(value.asText(), Qt::CaseSensitive) >= 0; } diff --git a/doomsday/sdk/libappfw/src/widgets/scriptcommandwidget.cpp b/doomsday/sdk/libappfw/src/widgets/scriptcommandwidget.cpp index 9bc0487622..ae4fd3a143 100644 --- a/doomsday/sdk/libappfw/src/widgets/scriptcommandwidget.cpp +++ b/doomsday/sdk/libappfw/src/widgets/scriptcommandwidget.cpp @@ -157,7 +157,7 @@ void ScriptCommandWidget::executeCommand(String const &text) try { Value const &result = d->process.context().evaluator().result(); - if (!result.is()) + if (!is(result)) { String msg = DENG2_CHAR_RIGHT_DOUBLEARROW " " _E(>)_E(m) + result.asText(); LOG_SCR_MSG(msg); diff --git a/doomsday/sdk/libappfw/src/widgets/variablechoicewidget.cpp b/doomsday/sdk/libappfw/src/widgets/variablechoicewidget.cpp index 2a1aa33d64..2b046ea65c 100644 --- a/doomsday/sdk/libappfw/src/widgets/variablechoicewidget.cpp +++ b/doomsday/sdk/libappfw/src/widgets/variablechoicewidget.cpp @@ -42,7 +42,7 @@ DENG2_OBSERVES(Variable, Change ) { if (!var) return; - if (var->value().is()) + if (is(var->value())) { self().setSelected(self().items().findData(var->value().asText())); } @@ -57,7 +57,7 @@ DENG2_OBSERVES(Variable, Change ) if (!var) return; var->audienceForChange() -= this; - if (var->value().is()) + if (is(var->value())) { var->set(TextValue(self().selectedItem().data().toString())); } diff --git a/doomsday/sdk/libappfw/src/widgets/variablesliderwidget.cpp b/doomsday/sdk/libappfw/src/widgets/variablesliderwidget.cpp index 5df3ebaf60..5891f224b2 100644 --- a/doomsday/sdk/libappfw/src/widgets/variablesliderwidget.cpp +++ b/doomsday/sdk/libappfw/src/widgets/variablesliderwidget.cpp @@ -94,7 +94,7 @@ VariableSliderWidget::VariableSliderWidget(Variable &variable, Ranged const &ran : SliderWidget(name) , d(new Impl(this, variable)) { - if (!variable.value().is()) + if (!is(variable.value())) { // Animation is the only other supported type. d->valueType = VariableSliderWidget::Animation; diff --git a/doomsday/sdk/libcore/include/de/ConditionalTrigger b/doomsday/sdk/libcore/include/de/ConditionalTrigger new file mode 100644 index 0000000000..ed6d76f890 --- /dev/null +++ b/doomsday/sdk/libcore/include/de/ConditionalTrigger @@ -0,0 +1 @@ +#include "data/conditionaltrigger.h" diff --git a/doomsday/sdk/libcore/include/de/Timeline b/doomsday/sdk/libcore/include/de/Timeline new file mode 100644 index 0000000000..a5f2cb9051 --- /dev/null +++ b/doomsday/sdk/libcore/include/de/Timeline @@ -0,0 +1 @@ +#include "scriptsys/timeline.h" diff --git a/doomsday/sdk/libcore/include/de/data/conditionaltrigger.h b/doomsday/sdk/libcore/include/de/data/conditionaltrigger.h new file mode 100644 index 0000000000..3d6054321c --- /dev/null +++ b/doomsday/sdk/libcore/include/de/data/conditionaltrigger.h @@ -0,0 +1,70 @@ +/** @file conditionaltrigger.h Conditional trigger configurable via a Variable. + * + * @authors Copyright (c) 2017 Jaakko Keränen + * + * @par License + * LGPL: http://www.gnu.org/licenses/lgpl.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 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 Lesser + * General Public License for more details. You should have received a copy of + * the GNU Lesser General Public License along with this program; if not, see: + * http://www.gnu.org/licenses + */ + +#ifndef LIBDENG2_CONDITIONALTRIGGER_H +#define LIBDENG2_CONDITIONALTRIGGER_H + +#include "../Variable" + +namespace de { + +/** + * Conditional trigger that calls a method + */ +class ConditionalTrigger +{ +public: + ConditionalTrigger(); + + virtual ~ConditionalTrigger(); + + bool isValid() const; + + /** + * Sets the variable that defines the condition for the trigger. The variable's + * value can be a Text value or an Array with multiple Text values. If any of the + * values is a single asterisk ("*"), the trigger will be activated with any input. + * + * @param variable Variable for configuring the conditional trigger. + */ + void setCondition(Variable const &variable); + + Variable const &condition() const; + + /** + * Checks if a trigger will cause activation, and if so, call the appropriate + * callback methods. + * + * @param trigger Trigger to check. + * @return @c true if the trigger was activated, otherwise @c false. + */ + bool tryTrigger(String const &trigger); + + /** + * Called when the trigger is activated. + * @param trigger Trigger that caused activation. + */ + virtual void handleTriggered(String const &trigger) = 0; + +private: + DENG2_PRIVATE(d) +}; + +} // namespace de + +#endif // LIBDENG2_CONDITIONALTRIGGER_H diff --git a/doomsday/sdk/libcore/include/de/data/value.h b/doomsday/sdk/libcore/include/de/data/value.h index 9207ef29aa..e0ee9dfd18 100644 --- a/doomsday/sdk/libcore/include/de/data/value.h +++ b/doomsday/sdk/libcore/include/de/data/value.h @@ -108,17 +108,18 @@ class DENG2_PUBLIC Value : public Deletable, public String::IPatternArg, public */ int asInt() const; + /** + * Convert the value to a list of strings using asText(). + * @return List of text strings. + */ + StringList asStringList() const; + /** * Convert the value to into a text string. All values have * to implement this. */ virtual Text asText() const = 0; - template - bool is() const { - return dynamic_cast(this) != 0; - } - template ValueType &as() { ValueType *t = dynamic_cast(this); @@ -139,26 +140,6 @@ class DENG2_PUBLIC Value : public Deletable, public String::IPatternArg, public return *t; } - /*template - static TargetType *maybeAs(ValueType *ptr) { - return dynamic_cast(ptr); - } - - template - static TargetType const *maybeAs(ValueType const *ptr) { - return dynamic_cast(ptr); - } - - template - static TargetType *maybeAs(ValueType &obj) { - return dynamic_cast(&obj); - } - - template - static TargetType const *maybeAs(ValueType const &obj) { - return dynamic_cast(&obj); - }*/ - /** * Returns the scope for any members of this value. When evaluating a member in * reference to this value, this will return the primary scope using which the diff --git a/doomsday/sdk/libcore/include/de/scriptsys/scheduler.h b/doomsday/sdk/libcore/include/de/scriptsys/timeline.h similarity index 77% rename from doomsday/sdk/libcore/include/de/scriptsys/scheduler.h rename to doomsday/sdk/libcore/include/de/scriptsys/timeline.h index 2ff4f6af2b..635ce193cd 100644 --- a/doomsday/sdk/libcore/include/de/scriptsys/scheduler.h +++ b/doomsday/sdk/libcore/include/de/scriptsys/timeline.h @@ -1,4 +1,4 @@ -/** @file scheduler.h Script scheduling utility. +/** @file timeline.h Script scheduling utility. * * @authors Copyright (c) 2015-2017 Jaakko Keränen * @@ -16,8 +16,8 @@ * http://www.gnu.org/licenses */ -#ifndef LIBDENG2_SCHEDULER_H -#define LIBDENG2_SCHEDULER_H +#ifndef LIBDENG2_TIMELINE_H +#define LIBDENG2_TIMELINE_H #include "../libcore.h" #include "../Time" @@ -29,24 +29,24 @@ class Script; class Record; /** - * Script scheduling utility. + * Collection of scripts to be run at specified points in time. * - * Scheduler owns the parsed scripts, but does not execute them. Use Scheduler::Clock - * to execute scripts. There can be any number of Scheduler::Clock instances operating - * on a single schedule. + * Timeline owns the parsed scripts, but does not execute them. Use Timeline::Clock + * to execute scripts. There can be any number of Timeline::Clock instances operating + * on a single timeline. * * @ingroup script */ -class DENG2_PUBLIC Scheduler +class DENG2_PUBLIC Timeline { public: - Scheduler(); + Timeline(); void clear(); /** * Sets the execution context, i.e., global namespace for the scripts. All - * scripts of the scheduler run in the same context. + * scripts of the timeline run in the same context. * * @param context Global namespace. */ @@ -55,22 +55,25 @@ class DENG2_PUBLIC Scheduler Record *context() const; /** - * Adds a new script to the scheduler. + * Adds a new script to the timeline. * * @param at Point in time when the script is to be executed. * @param source Script source. This will be parsed before the method returns. * @param sourcePath Path where the source comes from. * - * @return Scheduled Script (owned by Scheduler). + * @return Scheduled Script (owned by Timeline). */ Script &addScript(TimeDelta at, String const &source, String const &sourcePath = ""); void addFromInfo(Record const &timelineRecord); + /** + * Clock for executing a timeline. + */ class DENG2_PUBLIC Clock { public: - Clock(Scheduler const &schedule, Record *context = nullptr); + Clock(Timeline const &timeline, Record *context = nullptr); /** * Returns the current time of the clock. @@ -108,5 +111,5 @@ class DENG2_PUBLIC Scheduler } // namespace de -#endif // LIBDENG2_SCHEDULER_H +#endif // LIBDENG2_TIMELINE_H diff --git a/doomsday/sdk/libcore/src/data/conditionaltrigger.cpp b/doomsday/sdk/libcore/src/data/conditionaltrigger.cpp new file mode 100644 index 0000000000..da114e6813 --- /dev/null +++ b/doomsday/sdk/libcore/src/data/conditionaltrigger.cpp @@ -0,0 +1,108 @@ +/** @file conditionaltrigger.cpp Conditional trigger. + * + * @authors Copyright (c) 2017 Jaakko Keränen + * + * @par License + * LGPL: http://www.gnu.org/licenses/lgpl.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 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 Lesser + * General Public License for more details. You should have received a copy of + * the GNU Lesser General Public License along with this program; if not, see: + * http://www.gnu.org/licenses + */ + +#include "de/ConditionalTrigger" +#include "de/Value" + +#include + +namespace de { + +DENG2_PIMPL(ConditionalTrigger) +, DENG2_OBSERVES(Variable, Change) +{ + SafePtr condition; + QSet activeTriggers; + bool anyTrigger = false; + + Impl(Public *i) : Base(i) + {} + + void variableValueChanged(Variable &, Value const &) override + { + update(); + } + + void update() + { + anyTrigger = false; + activeTriggers.clear(); + + if (!condition) return; + + // The condition can be a text string or an array of text strings. + StringList const trigs = condition->value().asStringList(); + + for (String const &trig : trigs) + { + if (trig == "*") + { + anyTrigger = true; + activeTriggers.clear(); + break; + } + activeTriggers << trig; + } + } + + bool check(String const &trigger) + { + if (anyTrigger) return true; + return activeTriggers.contains(trigger); + } +}; + +ConditionalTrigger::ConditionalTrigger() + : d(new Impl(this)) +{} + +ConditionalTrigger::~ConditionalTrigger() +{} + +bool ConditionalTrigger::isValid() const +{ + return d->condition; +} + +void ConditionalTrigger::setCondition(Variable const &variable) +{ + if (d->condition) + { + d->condition->audienceForChange() -= d; + } + d->condition.reset(&variable); + variable.audienceForChange() += d; + d->update(); +} + +Variable const &ConditionalTrigger::condition() const +{ + return *d->condition; +} + +bool ConditionalTrigger::tryTrigger(String const &trigger) +{ + if (d->check(trigger)) + { + handleTriggered(trigger); + return true; + } + return false; +} + +} // namespace de diff --git a/doomsday/sdk/libcore/src/data/json.cpp b/doomsday/sdk/libcore/src/data/json.cpp index 7d8ad2c180..0da1ad79c9 100644 --- a/doomsday/sdk/libcore/src/data/json.cpp +++ b/doomsday/sdk/libcore/src/data/json.cpp @@ -324,7 +324,7 @@ static Block valueToJSONWithTabNewlines(Value const &value) static Block valueToJSON(Value const &value) { - if (value.is()) + if (is(value)) { return "null"; } diff --git a/doomsday/sdk/libcore/src/data/record.cpp b/doomsday/sdk/libcore/src/data/record.cpp index e13b6da062..7fc5bdcf65 100644 --- a/doomsday/sdk/libcore/src/data/record.cpp +++ b/doomsday/sdk/libcore/src/data/record.cpp @@ -770,7 +770,7 @@ Variable &Record::appendToArray(String const &name, Value *value) } Variable &var = (*this)[name]; - DENG2_ASSERT(var.value().is()); + DENG2_ASSERT(is(var.value())); var.value().add(value); return var; } diff --git a/doomsday/sdk/libcore/src/data/recordaccessor.cpp b/doomsday/sdk/libcore/src/data/recordaccessor.cpp index 96df80b2fe..a8e078b3f1 100644 --- a/doomsday/sdk/libcore/src/data/recordaccessor.cpp +++ b/doomsday/sdk/libcore/src/data/recordaccessor.cpp @@ -133,21 +133,7 @@ RecordValue const &RecordAccessor::getr(String const &name) const StringList RecordAccessor::getStringList(String const &name, StringList defaultValue) const { if (!accessedRecord().has(name)) return defaultValue; - - StringList str; - Value const &value = get(name); - if (value.is()) - { - for (Value const *val : value.as().elements()) - { - str << val->asText(); - } - } - else - { - str << value.asText(); - } - return str; + return get(name).asStringList(); } Record const &RecordAccessor::subrecord(String const &name) const diff --git a/doomsday/sdk/libcore/src/data/textvalue.cpp b/doomsday/sdk/libcore/src/data/textvalue.cpp index f0876a0d80..e5e7f05a26 100644 --- a/doomsday/sdk/libcore/src/data/textvalue.cpp +++ b/doomsday/sdk/libcore/src/data/textvalue.cpp @@ -71,7 +71,7 @@ bool TextValue::contains(Value const &value) const { // We are able to look for substrings within the text, without applying automatic // type conversions. - if (value.is()) + if (is(value)) { return _value.indexOf(value.asText(), Qt::CaseSensitive) >= 0; } diff --git a/doomsday/sdk/libcore/src/data/value.cpp b/doomsday/sdk/libcore/src/data/value.cpp index fe49c229e9..75a0c2f726 100644 --- a/doomsday/sdk/libcore/src/data/value.cpp +++ b/doomsday/sdk/libcore/src/data/value.cpp @@ -64,6 +64,23 @@ int Value::asInt() const return round(asNumber()); } +StringList Value::asStringList() const +{ + StringList str; + if (is(this)) + { + for (Value const *val : as().elements()) + { + str << val->asText(); + } + } + else + { + str << asText(); + } + return str; +} + Record *Value::memberScope() const { // By default, there are no members are thus no scope for them. diff --git a/doomsday/sdk/libcore/src/scriptsys/process.cpp b/doomsday/sdk/libcore/src/scriptsys/process.cpp index c81931723a..d06d9cac20 100644 --- a/doomsday/sdk/libcore/src/scriptsys/process.cpp +++ b/doomsday/sdk/libcore/src/scriptsys/process.cpp @@ -387,7 +387,7 @@ void Process::call(Function const &function, ArrayValue const &arguments, Value for (; b != argValues.end() && a != function.arguments().end(); ++b, ++a) { // Records must only be passed as unowned references. - DENG2_ASSERT(!(*b)->is() || !(*b)->as().hasOwnership()); + DENG2_ASSERT(!is(*b) || !(*b)->as().hasOwnership()); context().names().add(new Variable(*a, (*b)->duplicate())); } diff --git a/doomsday/sdk/libcore/src/scriptsys/scheduler.cpp b/doomsday/sdk/libcore/src/scriptsys/timeline.cpp similarity index 84% rename from doomsday/sdk/libcore/src/scriptsys/scheduler.cpp rename to doomsday/sdk/libcore/src/scriptsys/timeline.cpp index abb77562e6..5b6ebbce42 100644 --- a/doomsday/sdk/libcore/src/scriptsys/scheduler.cpp +++ b/doomsday/sdk/libcore/src/scriptsys/timeline.cpp @@ -16,7 +16,7 @@ * http://www.gnu.org/licenses */ -#include "de/Scheduler" +#include "de/Timeline" #include "de/ScriptedInfo" #include "de/Record" #include "de/Script" @@ -27,7 +27,7 @@ namespace de { -DENG2_PIMPL(Scheduler) +DENG2_PIMPL(Timeline) , DENG2_OBSERVES(Record, Deletion) { Record *context = nullptr; @@ -84,28 +84,28 @@ DENG2_PIMPL(Scheduler) } }; -Scheduler::Scheduler() +Timeline::Timeline() : d(new Impl(this)) {} -void Scheduler::clear() +void Timeline::clear() { d->clear(); } -void Scheduler::setContext(Record &context) +void Timeline::setContext(Record &context) { d->setContext(&context); } -Script &Scheduler::addScript(TimeDelta at, String const &source, String const &sourcePath) +Script &Timeline::addScript(TimeDelta at, String const &source, String const &sourcePath) { auto *ev = new Impl::Event(at, source, sourcePath); d->events.push(ev); return ev->script; } -void Scheduler::addFromInfo(Record const &timelineRecord) +void Timeline::addFromInfo(Record const &timelineRecord) { auto scripts = ScriptedInfo::subrecordsOfType(ScriptedInfo::SCRIPT, timelineRecord); for (String key : ScriptedInfo::sortRecordsBySource(scripts)) @@ -128,13 +128,13 @@ void Scheduler::addFromInfo(Record const &timelineRecord) //---------------------------------------------------------------------------- -DENG2_PIMPL_NOREF(Scheduler::Clock) +DENG2_PIMPL_NOREF(Timeline::Clock) { - typedef Scheduler::Impl::Event Event; - typedef Scheduler::Impl::Events Events; // Events not owned + typedef Timeline::Impl::Event Event; + typedef Timeline::Impl::Events Events; // Events not owned Record *context = nullptr; - Scheduler const *scheduler = nullptr; + Timeline const *scheduler = nullptr; TimeDelta at = 0.0; Events events; @@ -173,7 +173,7 @@ DENG2_PIMPL_NOREF(Scheduler::Clock) } }; -Scheduler::Clock::Clock(Scheduler const &schedule, Record *context) +Timeline::Clock::Clock(Timeline const &schedule, Record *context) : d(new Impl) { d->scheduler = &schedule; @@ -181,22 +181,22 @@ Scheduler::Clock::Clock(Scheduler const &schedule, Record *context) d->rewind(0.0); } -TimeDelta Scheduler::Clock::at() const +TimeDelta Timeline::Clock::at() const { return d->at; } -void Scheduler::Clock::rewind(TimeDelta const &toTime) +void Timeline::Clock::rewind(TimeDelta const &toTime) { d->rewind(toTime); } -void Scheduler::Clock::advanceTime(TimeDelta const &elapsed) +void Timeline::Clock::advanceTime(TimeDelta const &elapsed) { d->advanceTime(elapsed); } -bool Scheduler::Clock::isFinished() const +bool Timeline::Clock::isFinished() const { return d->events.empty(); }