mirror of
https://github.com/AleziaKurdis/overte.git
synced 2025-04-08 06:32:35 +02:00
Merge pull request #847 from humbletim/modkit-example-revamp
Example "modkit" JS Plugin renovation
This commit is contained in:
commit
ec36621d40
9 changed files with 261 additions and 226 deletions
|
@ -312,9 +312,9 @@ Rectangle {
|
|||
parent.color = hifi.colors.blueHighlight;
|
||||
}
|
||||
onClicked: {
|
||||
lightboxPopup.titleText = "Script Plugin Infrastructure by Kasen";
|
||||
lightboxPopup.titleText = "Script Plugin Infrastructure";
|
||||
lightboxPopup.bodyText = "Toggles the activation of scripting plugins in the 'plugins/scripting' folder. \n\n"
|
||||
+ "Created by https://kasen.io/";
|
||||
+ "Created by:\n humbletim@gmail.com\n kasenvr@gmail.com";
|
||||
lightboxPopup.button1text = "OK";
|
||||
lightboxPopup.button1method = function() {
|
||||
lightboxPopup.visible = false;
|
||||
|
|
|
@ -53,5 +53,5 @@ set(DIR "opusCodec")
|
|||
add_subdirectory(${DIR})
|
||||
|
||||
# example plugins
|
||||
set(DIR "KasenAPIExample")
|
||||
set(DIR "JSAPIExample")
|
||||
add_subdirectory(${DIR})
|
||||
|
|
3
plugins/JSAPIExample/CMakeLists.txt
Normal file
3
plugins/JSAPIExample/CMakeLists.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
set(TARGET_NAME JSAPIExample)
|
||||
setup_hifi_client_server_plugin(scripting)
|
||||
link_hifi_libraries(shared plugins)
|
251
plugins/JSAPIExample/src/JSAPIExample.cpp
Normal file
251
plugins/JSAPIExample/src/JSAPIExample.cpp
Normal file
|
@ -0,0 +1,251 @@
|
|||
//
|
||||
// JSAPIExample.cpp
|
||||
// plugins/JSAPIExample/src
|
||||
//
|
||||
// Copyright (c) 2019-2020 humbletim (humbletim@gmail.com)
|
||||
// Copyright (c) 2019 Kalila L. (kasenvr@gmail.com)
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// Example of prototyping new JS APIs by leveraging the existing plugin system.
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QLoggingCategory>
|
||||
#include <QtCore/QThread>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtScript/QScriptEngine>
|
||||
#include <QtScript/QScriptable>
|
||||
|
||||
#include <SettingHelpers.h> // for ::settingsFilename()
|
||||
#include <SharedUtil.h> // for ::usecTimestampNow()
|
||||
#include <shared/ScriptInitializerMixin.h>
|
||||
|
||||
// NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes)
|
||||
namespace REPLACE_ME_WITH_UNIQUE_NAME {
|
||||
|
||||
static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1";
|
||||
static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample";
|
||||
|
||||
QLoggingCategory logger { "jsapiexample" };
|
||||
|
||||
inline QVariant raiseScriptingError(QScriptContext* context, const QString& message, const QVariant& returnValue = QVariant()) {
|
||||
if (context) {
|
||||
// when a QScriptContext is available throw an actual JS Exception (which can be caught using try/catch on JS side)
|
||||
context->throwError(message);
|
||||
} else {
|
||||
// otherwise just log the error
|
||||
qCWarning(logger) << "error:" << message;
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error);
|
||||
|
||||
class JSAPIExample : public QObject, public QScriptable {
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "JSAPIExample" FILE "plugin.json")
|
||||
Q_PROPERTY(QString version MEMBER _version CONSTANT)
|
||||
public:
|
||||
JSAPIExample() {
|
||||
setObjectName(JSAPI_EXPORT_NAME);
|
||||
auto scriptInit = DependencyManager::get<ScriptInitializers>();
|
||||
if (!scriptInit) {
|
||||
qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this;
|
||||
return;
|
||||
}
|
||||
qCWarning(logger) << "registering w/ScriptInitializerMixin..." << scriptInit.data();
|
||||
scriptInit->registerScriptInitializer([this](QScriptEngine* engine) {
|
||||
auto value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater);
|
||||
engine->globalObject().setProperty(objectName(), value);
|
||||
// qCDebug(logger) << "setGlobalInstance" << objectName() << engine->property("fileName");
|
||||
});
|
||||
// qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread();
|
||||
}
|
||||
|
||||
// NOTES: everything within the "public slots:" section below will be available from JS via overall plugin QObject
|
||||
// also, to demonstrate future-proofing JS API code, QVariant's are used throughout most of these examples --
|
||||
// which still makes them very Qt-specific, but avoids depending directly on deprecated QtScript/QScriptValue APIs.
|
||||
// (as such this plugin class and its methods remain forward-compatible with other engines like QML's QJSEngine)
|
||||
|
||||
public slots:
|
||||
// pretty-printed representation for logging eg: print(JSAPIExample)
|
||||
// (note: Qt script engines automatically look for a ".toString" method on native classes when coercing values to strings)
|
||||
QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); }
|
||||
|
||||
/**jsdoc
|
||||
* Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms
|
||||
* @example <caption>Measure current setTimeout accuracy.</caption>
|
||||
* var expected = 1000;
|
||||
* var start = JSAPIExample.now();
|
||||
* Script.setTimeout(function () {
|
||||
* var elapsed = (JSAPIExample.now() - start)/1000;
|
||||
* print("expected (ms):", expected, "actual (ms):", elapsed);
|
||||
* }, expected);
|
||||
*/
|
||||
QVariant now() const { return usecTimestampNow(); }
|
||||
|
||||
/**jsdoc
|
||||
* Example of returning a JS Object key-value map
|
||||
* @example <caption>"zip" a list of keys and corresponding values to form key-value map</caption>
|
||||
* print(JSON.stringify(JSAPIExample.zip(["a","b"], [1,2])); // { "a": 1, "b": 2 }
|
||||
*/
|
||||
QVariant zip(const QStringList& keys, const QVariantList& values) const {
|
||||
QVariantMap out;
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
out[keys[i]] = i < values.size() ? values[i] : QVariant();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**jsdoc
|
||||
* Example of returning a JS Array result
|
||||
* @example <caption>emulate Object.values(keyValues)</caption>
|
||||
* print(JSON.stringify(JSAPIExample.values({ "a": 1, "b": 2 }))); // [1,2]
|
||||
*/
|
||||
QVariant values(const QVariantMap& keyValues) const {
|
||||
QVariantList values = keyValues.values();
|
||||
return values;
|
||||
}
|
||||
|
||||
/**jsdoc
|
||||
* Another example of returning JS Array data
|
||||
* @example <caption>generate an integer sequence (inclusive of [from, to])</caption>
|
||||
* print(JSON.stringify(JSAPIExample.seq(1,5)));// [1,2,3,4,5]
|
||||
*/
|
||||
QVariant seq(int from, int to) const {
|
||||
QVariantList out;
|
||||
for (int i = from; i <= to; i++) {
|
||||
out.append(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**jsdoc
|
||||
* Example of returning arbitrary binary data from C++ (resulting in a JS ArrayBuffer)
|
||||
* see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#Examples
|
||||
* @example <caption>return compressed/decompressed versions of the input data</caption>
|
||||
* var data = "testing 1 2 3";
|
||||
* var z = JSAPIExample.qCompressString(data); // z will be an ArrayBuffer
|
||||
* var u = JSAPIExample.qUncompressString(z); // u will be a String value
|
||||
* print(JSON.stringify({ input: data, compressed: z.byteLength, output: u, uncompressed: u.length }));
|
||||
*/
|
||||
QVariant qCompressString(const QString& jsString, int compress_level = -1) const {
|
||||
QByteArray arrayBuffer = qCompress(jsString.toUtf8(), compress_level);
|
||||
return arrayBuffer;
|
||||
}
|
||||
QVariant qUncompressString(const QByteArray& arrayBuffer) const {
|
||||
QString jsString = QString::fromUtf8(qUncompress(arrayBuffer));
|
||||
return jsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of exposing a custom "managed" C++ QObject to JS
|
||||
* The lifecycle of the created QObject* instance becomes managed by the invoking QScriptEngine --
|
||||
* it will be automatically cleaned up once no longer reachable from any JS variables/closures.
|
||||
* @example <caption>access persistent settings stored in separate .json files</caption>
|
||||
* var settings = JSAPIExample.getScopedSettings("example");
|
||||
* print("example settings stored in:", settings.fileName());
|
||||
* print("(before) example::timestamp", settings.getValue("timestamp"));
|
||||
* settings.setValue("timestamp", Date.now());
|
||||
* print("(after) example::timestamp", settings.getValue("timestamp"));
|
||||
* print("all example::* keys", settings.allKeys());
|
||||
* settings = null; // optional best pratice; allows the object to be reclaimed ASAP by the JS garbage collector
|
||||
*/
|
||||
QScriptValue getScopedSettings(const QString& scope) {
|
||||
auto engine = QScriptable::engine();
|
||||
if (!engine) {
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
QString error;
|
||||
auto cppValue = createScopedSettings(scope, engine, error);
|
||||
if (!cppValue) {
|
||||
raiseScriptingError(context(), "error creating scoped settings instance: " + error);
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater);
|
||||
}
|
||||
|
||||
private:
|
||||
const QString _version { JSAPI_SEMANTIC_VERSION };
|
||||
};
|
||||
|
||||
// JSSettingsHelper wraps a scoped (prefixed/separate) QSettings and exposes a subset of QSetting APIs as slots
|
||||
class JSSettingsHelper : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
JSSettingsHelper(const QString& scope, QObject* parent = nullptr);
|
||||
~JSSettingsHelper();
|
||||
operator bool() const;
|
||||
public slots:
|
||||
QString fileName() const;
|
||||
QString toString() const;
|
||||
QVariant getValue(const QString& key, const QVariant& defaultValue = QVariant());
|
||||
bool setValue(const QString& key, const QVariant& value);
|
||||
QStringList allKeys() const;
|
||||
protected:
|
||||
QString _scope;
|
||||
QString _fileName;
|
||||
QSharedPointer<QSettings> _settings;
|
||||
QString getLocalSettingsPath(const QString& scope) const;
|
||||
};
|
||||
|
||||
// verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance
|
||||
QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error) {
|
||||
const QRegExp VALID_SETTINGS_SCOPE { "[-_A-Za-z0-9]{1,64}" };
|
||||
if (!VALID_SETTINGS_SCOPE.exactMatch(scope)) {
|
||||
error = QString("invalid scope (expected alphanumeric <= 64 chars not '%1')").arg(scope);
|
||||
return nullptr;
|
||||
}
|
||||
return new JSSettingsHelper(scope, parent);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// ----- inline JSSettingsHelper implementation -----
|
||||
JSSettingsHelper::operator bool() const {
|
||||
return (bool)_settings;
|
||||
}
|
||||
JSSettingsHelper::JSSettingsHelper(const QString& scope, QObject* parent) :
|
||||
QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)),
|
||||
_settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) {
|
||||
}
|
||||
JSSettingsHelper::~JSSettingsHelper() {
|
||||
qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this;
|
||||
}
|
||||
QString JSSettingsHelper::fileName() const {
|
||||
return _settings ? _settings->fileName() : "";
|
||||
}
|
||||
QString JSSettingsHelper::toString() const {
|
||||
return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings);
|
||||
}
|
||||
QVariant JSSettingsHelper::getValue(const QString& key, const QVariant& defaultValue) {
|
||||
return _settings ? _settings->value(key, defaultValue) : defaultValue;
|
||||
}
|
||||
bool JSSettingsHelper::setValue(const QString& key, const QVariant& value) {
|
||||
if (_settings) {
|
||||
if (value.isValid()) {
|
||||
_settings->setValue(key, value);
|
||||
} else {
|
||||
_settings->remove(key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
QStringList JSSettingsHelper::allKeys() const {
|
||||
return _settings ? _settings->allKeys() : QStringList{};
|
||||
}
|
||||
QString JSSettingsHelper::getLocalSettingsPath(const QString& scope) const {
|
||||
// generate a prefixed filename (relative to the main application's Interface.json file)
|
||||
const QString fileName = QString("jsapi_%1.json").arg(scope);
|
||||
return QFileInfo(::settingsFilename()).dir().filePath(fileName);
|
||||
}
|
||||
// ----- /inline JSSettingsHelper implementation -----
|
||||
|
||||
} // namespace REPLACE_ME_WITH_UNIQUE_NAME
|
||||
|
||||
#include "JSAPIExample.moc"
|
4
plugins/JSAPIExample/src/plugin.json
Normal file
4
plugins/JSAPIExample/src/plugin.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name":"JS API Example",
|
||||
"version": 1
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
set(TARGET_NAME KasenAPIExample)
|
||||
setup_hifi_client_server_plugin(scripting)
|
||||
link_hifi_libraries(shared plugins avatars networking graphics gpu)
|
|
@ -1,58 +0,0 @@
|
|||
//
|
||||
// ExampleScriptPlugin.h
|
||||
// plugins/KasenAPIExample/src
|
||||
//
|
||||
// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com
|
||||
// Copyright 2019 Kasen IO
|
||||
//
|
||||
// Authored by: Humbletim (humbletim@gmail.com)
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
// Supporting file containing all QtScript specific integration.
|
||||
|
||||
#ifndef EXAMPLE_SCRIPT_PLUGIN_H
|
||||
#define EXAMPLE_SCRIPT_PLUGIN_H
|
||||
|
||||
#if DEV_BUILD
|
||||
#pragma message("QtScript is deprecated see: doc.qt.io/qt-5/topics-scripting.html")
|
||||
#endif
|
||||
#include <QtScript/QScriptEngine>
|
||||
|
||||
#include <QtCore/QLoggingCategory>
|
||||
#include <QCoreApplication>
|
||||
#include <shared/ScriptInitializerMixin.h>
|
||||
|
||||
namespace example {
|
||||
|
||||
extern const QLoggingCategory& logger;
|
||||
|
||||
inline void setGlobalInstance(QScriptEngine* engine, const QString& name, QObject* object) {
|
||||
auto value = engine->newQObject(object, QScriptEngine::QtOwnership);
|
||||
engine->globalObject().setProperty(name, value);
|
||||
qCDebug(logger) << "setGlobalInstance" << name << engine->property("fileName");
|
||||
}
|
||||
|
||||
class ScriptPlugin : public QObject {
|
||||
Q_OBJECT
|
||||
QString _version;
|
||||
Q_PROPERTY(QString version MEMBER _version CONSTANT)
|
||||
protected:
|
||||
inline ScriptPlugin(const QString& name, const QString& version) : _version(version) {
|
||||
setObjectName(name);
|
||||
if (!DependencyManager::get<ScriptInitializers>()) {
|
||||
qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this;
|
||||
return;
|
||||
}
|
||||
qCWarning(logger) << "registering w/ScriptInitializerMixin..." << DependencyManager::get<ScriptInitializers>().data();
|
||||
DependencyManager::get<ScriptInitializers>()->registerScriptInitializer(
|
||||
[this](QScriptEngine* engine) { setGlobalInstance(engine, objectName(), this); });
|
||||
}
|
||||
public slots:
|
||||
inline QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); }
|
||||
};
|
||||
|
||||
} // namespace example
|
||||
|
||||
#endif
|
|
@ -1,141 +0,0 @@
|
|||
//
|
||||
// KasenAPIExample.cpp
|
||||
// plugins/KasenAPIExample/src
|
||||
//
|
||||
// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com
|
||||
// Copyright 2019 Kasen IO
|
||||
//
|
||||
// Authored by: Humbletim (humbletim@gmail.com)
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
// Example of prototyping new JS APIs by leveraging the existing plugin system.
|
||||
|
||||
#include "ExampleScriptPlugin.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QLoggingCategory>
|
||||
#include <QtCore/QThread>
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
#include <SharedUtil.h>
|
||||
#include <AvatarHashMap.h>
|
||||
|
||||
namespace custom_api_example {
|
||||
|
||||
QLoggingCategory logger{ "custom_api_example" };
|
||||
|
||||
class KasenAPIExample : public example::ScriptPlugin {
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "KasenAPIExample" FILE "plugin.json")
|
||||
public:
|
||||
KasenAPIExample() : example::ScriptPlugin("KasenAPIExample", "0.0.1") {
|
||||
qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread();
|
||||
}
|
||||
|
||||
public slots:
|
||||
/**jsdoc
|
||||
* Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms
|
||||
* @example <caption>Measure current setTimeout accuracy.</caption>
|
||||
* var expected = 1000;
|
||||
* var start = KasenAPIExample.now();
|
||||
* Script.setTimeout(function () {
|
||||
* var elapsed = (KasenAPIExample.now() - start)/1000;
|
||||
* print("expected (ms):", expected, "actual (ms):", elapsed);
|
||||
* }, expected);
|
||||
*/
|
||||
QVariant now() const {
|
||||
return usecTimestampNow();
|
||||
}
|
||||
|
||||
/**jsdoc
|
||||
* Returns the available blendshape names for an avatar.
|
||||
* @example <caption>Get blendshape names</caption>
|
||||
* print(JSON.stringify(KasenAPIExample.getBlendshapeNames(MyAvatar.sessionUUID)));
|
||||
*/
|
||||
QStringList getBlendshapeNames(const QUuid& avatarID) const {
|
||||
QVector<QString> out;
|
||||
if (auto head = getAvatarHead(avatarID)) {
|
||||
for (const auto& kv : head->getBlendshapeMap().toStdMap()) {
|
||||
if (kv.second >= out.size()) out.resize(kv.second+1);
|
||||
out[kv.second] = kv.first;
|
||||
}
|
||||
}
|
||||
return out.toList();
|
||||
}
|
||||
|
||||
/**jsdoc
|
||||
* Returns a key-value object with active (non-zero) blendshapes.
|
||||
* eg: { JawOpen: 1.0, ... }
|
||||
* @example <caption>Get active blendshape map</caption>
|
||||
* print(JSON.stringify(KasenAPIExample.getActiveBlendshapes(MyAvatar.sessionUUID)));
|
||||
*/
|
||||
QVariant getActiveBlendshapes(const QUuid& avatarID) const {
|
||||
if (auto head = getAvatarHead(avatarID)) {
|
||||
return head->toJson()["blendShapes"].toVariant();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QVariant getBlendshapeMapping(const QUuid& avatarID) const {
|
||||
QVariantMap out;
|
||||
if (auto head = getAvatarHead(avatarID)) {
|
||||
for (const auto& kv : head->getBlendshapeMap().toStdMap()) {
|
||||
out[kv.first] = kv.second;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QVariant getBlendshapes(const QUuid& avatarID) const {
|
||||
QVariantMap result;
|
||||
if (auto head = getAvatarHead(avatarID)) {
|
||||
QStringList names = getBlendshapeNames(avatarID);
|
||||
auto states = head->getBlendshapeStates();
|
||||
result = {
|
||||
{ "base", zipNonZeroValues(names, states.base) },
|
||||
{ "summed", zipNonZeroValues(names, states.summed) },
|
||||
{ "transient", zipNonZeroValues(names, states.transient) },
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
static QVariantMap zipNonZeroValues(const QStringList& keys, const QVector<float>& values) {
|
||||
QVariantMap out;
|
||||
for (int i=1; i < values.size(); i++) {
|
||||
if (fabs(values[i]) > 1.0e-6f) {
|
||||
out[keys.value(i)] = values[i];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
struct _HeadHelper : public HeadData {
|
||||
QMap<QString,int> getBlendshapeMap() const {
|
||||
return BLENDSHAPE_LOOKUP_MAP;
|
||||
}
|
||||
struct States { QVector<float> base, summed, transient; };
|
||||
States getBlendshapeStates() const {
|
||||
return {
|
||||
_blendshapeCoefficients,
|
||||
_summedBlendshapeCoefficients,
|
||||
_transientBlendshapeCoefficients
|
||||
};
|
||||
}
|
||||
};
|
||||
static const _HeadHelper* getAvatarHead(const QUuid& avatarID) {
|
||||
auto avatars = DependencyManager::get<AvatarHashMap>();
|
||||
auto avatar = avatars ? avatars->getAvatarBySessionID(avatarID) : nullptr;
|
||||
auto head = avatar ? avatar->getHeadData() : nullptr;
|
||||
return reinterpret_cast<const _HeadHelper*>(head);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const QLoggingCategory& example::logger{ custom_api_example::logger };
|
||||
|
||||
#include "KasenAPIExample.moc"
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name":"Kasen JS API Example",
|
||||
"version": 1,
|
||||
"package": {
|
||||
"author": "Revofire",
|
||||
"homepage": "www.realities.dev",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"hifi-interface": ">= 0.83.0",
|
||||
"hifi-assignment-client": ">= 0.83.0"
|
||||
},
|
||||
"config": {
|
||||
"client": true,
|
||||
"entity_client": true,
|
||||
"entity_server": true,
|
||||
"edit_filter": true,
|
||||
"agent": true,
|
||||
"avatar": true
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue