Merge branch 'master' of github.com:highfidelity/hifi into yellow

This commit is contained in:
Sam Gateau 2019-09-12 17:01:37 -07:00
commit 802326d4da
52 changed files with 1191 additions and 352 deletions

View file

@ -84,7 +84,7 @@ Agent::Agent(ReceivedMessage& message) :
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
DependencyManager::set<ResourceManager>();
DependencyManager::set<PluginManager>();
DependencyManager::set<PluginManager>()->instantiate();
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
@ -511,6 +511,7 @@ void Agent::executeScript() {
DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
DependencyManager::get<ScriptEngines>()->runScriptInitializers(_scriptEngine);
_scriptEngine->run();
Frame::clearFrameHandler(AUDIO_FRAME_TYPE);

View file

@ -22,6 +22,7 @@
#include <HifiConfigVariantMap.h>
#include <SharedUtil.h>
#include <ShutdownEventListener.h>
#include <shared/ScriptInitializerMixin.h>
#include "Assignment.h"
#include "AssignmentClient.h"
@ -240,6 +241,7 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
QThread::currentThread()->setObjectName("main thread");
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
DependencyManager::set<ScriptInitializers>();
if (numForks || minForks || maxForks) {
AssignmentClientMonitor* monitor = new AssignmentClientMonitor(numForks, minForks, maxForks,

View file

@ -19,6 +19,7 @@
#include <EntityTree.h>
#include <ResourceCache.h>
#include <ScriptCache.h>
#include <plugins/PluginManager.h>
#include <EntityEditFilters.h>
#include <NetworkingConstants.h>
#include <hfm/ModelFormatRegistry.h>
@ -41,6 +42,7 @@ EntityServer::EntityServer(ReceivedMessage& message) :
DependencyManager::set<ResourceManager>();
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<ScriptCache>();
DependencyManager::set<PluginManager>()->instantiate();
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
DependencyManager::set<AssignmentDynamicFactory>();

View file

@ -64,7 +64,7 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig
DependencyManager::set<ResourceScriptingInterface>();
DependencyManager::set<ResourceManager>();
DependencyManager::set<PluginManager>();
DependencyManager::set<PluginManager>()->instantiate();
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
@ -462,7 +462,7 @@ void EntityScriptServer::resetEntitiesScriptEngine() {
_entityViewer.getTree()->update();
});
scriptEngines->runScriptInitializers(newEngine);
newEngine->runInThread();
auto newEngineSP = qSharedPointerCast<EntitiesScriptEngineProvider>(newEngine);
DependencyManager::get<EntityScriptingInterface>()->setEntitiesScriptEngine(newEngineSP);

View file

@ -7,7 +7,8 @@
#
macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN)
set(${TARGET_NAME}_SHARED 1)
setup_hifi_library(${ARGV})
set(PLUGIN_SUBFOLDER ${ARGN})
setup_hifi_library()
if (BUILD_CLIENT)
add_dependencies(interface ${TARGET_NAME})
@ -27,6 +28,11 @@ macro(SETUP_HIFI_CLIENT_SERVER_PLUGIN)
set(SERVER_PLUGIN_PATH "plugins")
endif()
if (PLUGIN_SUBFOLDER)
set(CLIENT_PLUGIN_PATH "${CLIENT_PLUGIN_PATH}/${PLUGIN_SUBFOLDER}")
set(SERVER_PLUGIN_PATH "${SERVER_PLUGIN_PATH}/${PLUGIN_SUBFOLDER}")
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_GENERATOR STREQUAL "Unix Makefiles")
set(CLIENT_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/${CLIENT_PLUGIN_PATH}/")
set(SERVER_PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/assignment-client/${SERVER_PLUGIN_PATH}/")

View file

@ -1156,7 +1156,17 @@ FunctionEnd
Section "-Core installation"
;The following delete blocks are temporary and can be removed once users who had the initial installer have updated
; 2016-02-25 - The following delete blocks are temporary and can be removed once users who had the initial installer have updated
; 2019-09-10 - (3 and a half years later) Sure they are buddy. Sure they are.
; MessageBox MB_OK|MB_ICONEXCLAMATION "installer type is @INSTALLER_TYPE@"
;Delete any server executables that might have been installed by bad versions of the client-only installer, but ONLY if we are a client-only installer
${If} "@INSTALLER_TYPE@" == "client_only"
; MessageBox MB_OK|MB_ICONEXCLAMATION "trying to delete server binaries"
Delete "$INSTDIR\assignment-client.exe"
Delete "$INSTDIR\domain-server.exe"
${EndIf}
;Delete any server-console files installed before it was placed in sub-folder
Delete "$INSTDIR\server-console.exe"

View file

@ -1,4 +1,4 @@
<!-- Copyright 2016 High Fidelity, Inc. -->
<!-- Copyright 2016 High Fidelity, Inc. -->
<html>
<head>
<meta charset="utf-8"/>
@ -77,9 +77,9 @@
var handControllerImageURL = null;
var index = 0;
var count = 3;
var handControllerRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#vr-controls";
var keyboardRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/desktop.html#movement-controls";
var gamepadRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#gamepad";
var handControllerRefURL = "https://docs.highfidelity.com/explore/get-started/vr-controls.html#vr-controls";
var keyboardRefURL = "https://docs.highfidelity.com/explore/get-started/desktop.html#movement-controls";
var gamepadRefURL = "https://docs.highfidelity.com/explore/get-started/vr-controls.html#gamepad";
function showKbm() {
document.getElementById("main_image").setAttribute("src", "img/tablet-help-keyboard.jpg");

View file

@ -266,6 +266,7 @@ Rectangle {
labelTextSize: 16;
backgroundOnColor: "#E3E3E3";
checked: AudioScriptingInterface.warnWhenMuted;
visible: bar.currentIndex !== 0;
onClicked: {
AudioScriptingInterface.warnWhenMuted = checked;
checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding
@ -277,8 +278,8 @@ Rectangle {
id: audioLevelSwitch
height: root.switchHeight;
switchWidth: root.switchWidth;
anchors.top: warnMutedSwitch.bottom
anchors.topMargin: 24
anchors.top: warnMutedSwitch.visible ? warnMutedSwitch.bottom : parent.top
anchors.topMargin: bar.currentIndex === 0 ? 0 : 24
anchors.left: parent.left
labelTextOn: qsTr("Audio Level Meter");
labelTextSize: 16;

View file

@ -240,9 +240,96 @@ Rectangle {
}
}
// -- Plugin Permissions --
Item {
id: kpiContainer;
anchors.top: accountContainer.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
height: childrenRect.height;
Rectangle {
id: kpiHeaderContainer;
anchors.top: parent.top;
anchors.left: parent.left;
anchors.right: parent.right;
height: 55;
color: hifi.colors.baseGrayHighlight;
HifiStylesUit.RalewaySemiBold {
text: "Plugin Permissions";
anchors.fill: parent;
anchors.leftMargin: 20;
color: hifi.colors.white;
size: 18;
}
}
Item {
id: kpiScriptContainer;
anchors.top: kpiHeaderContainer.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
height: 80;
HifiControlsUit.CheckBox {
id: kpiScriptCheckbox;
readonly property string kpiSettingsKey: "private/enableScriptingPlugins"
checked: Settings.getValue(kpiSettingsKey, false);
text: "Enable custom script plugins (requires restart)"
// Anchors
anchors.verticalCenter: parent.verticalCenter;
anchors.left: parent.left;
anchors.leftMargin: 20;
boxSize: 24;
labelFontSize: 18;
colorScheme: hifi.colorSchemes.dark
color: hifi.colors.white;
width: 300;
onCheckedChanged: Settings.setValue(kpiSettingsKey, checked);
}
HifiStylesUit.RalewaySemiBold {
id: kpiScriptHelp;
text: '[?]';
// Anchors
anchors.verticalCenter: parent.verticalCenter;
anchors.left: kpiScriptCheckbox.right;
width: 30;
height: 30;
// Text size
size: 18;
// Style
color: hifi.colors.blueHighlight;
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
onEntered: {
parent.color = hifi.colors.blueAccent;
}
onExited: {
parent.color = hifi.colors.blueHighlight;
}
onClicked: {
lightboxPopup.titleText = "Script Plugin Infrastructure by Kasen";
lightboxPopup.bodyText = "Toggles the activation of scripting plugins in the 'plugins/scripting' folder. \n\n"
+ "Created by https://kasen.io/";
lightboxPopup.button1text = "OK";
lightboxPopup.button1method = function() {
lightboxPopup.visible = false;
}
lightboxPopup.visible = true;
}
}
}
}
}
Item {
id: walletContainer;
anchors.top: accountContainer.bottom;
anchors.top: kpiContainer.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
height: childrenRect.height;

View file

@ -167,7 +167,7 @@ Flickable {
Component.onCompleted: {
var cpu = JSON.parse(PlatformInfo.getCPU(0));
var cpuModel = cpu.model;
if (cpuModel.length === 0) {
if (!cpuModel || cpuModel.length === 0) {
cpuModel = "Unknown";
}
@ -213,7 +213,7 @@ Flickable {
Component.onCompleted: {
var gpu = JSON.parse(PlatformInfo.getGPU(PlatformInfo.getMasterGPU()));
var gpuModel = gpu.model;
if (gpuModel.length === 0) {
if (!gpuModel || gpuModel.length === 0) {
gpuModel = "Unknown";
}
@ -327,7 +327,7 @@ Flickable {
var cpu = JSON.parse(PlatformInfo.getCPU(0));
var cpuModel = cpu.model;
if (cpuModel.length === 0) {
if (!cpuModel || cpuModel.length === 0) {
cpuModel = "Unknown";
}
@ -338,7 +338,7 @@ Flickable {
var gpu = JSON.parse(PlatformInfo.getGPU(PlatformInfo.getMasterGPU()));
var gpuModel = gpu.model;
if (gpuModel.length === 0) {
if (!gpuModel || gpuModel.length === 0) {
gpuModel = "Unknown";
}

View file

@ -810,6 +810,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
}
// Tell the plugin manager about our statically linked plugins
DependencyManager::set<ScriptInitializers>();
DependencyManager::set<PluginManager>();
auto pluginManager = PluginManager::getInstance();
pluginManager->setInputPluginProvider([] { return getInputPlugins(); });
@ -859,7 +860,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
#endif
DependencyManager::set<StatTracker>();
DependencyManager::set<ScriptEngines>(ScriptEngine::CLIENT_SCRIPT, defaultScriptsOverrideOption);
DependencyManager::set<ScriptInitializerMixin, NativeScriptInitializers>();
DependencyManager::set<Preferences>();
DependencyManager::set<recording::Deck>();
DependencyManager::set<recording::Recorder>();
@ -3425,7 +3425,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
surfaceContext->setContextProperty("Window", DependencyManager::get<WindowScriptingInterface>().data());
surfaceContext->setContextProperty("Desktop", DependencyManager::get<DesktopScriptingInterface>().data());
surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance());
surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance());
surfaceContext->setContextProperty("Settings", new QMLSettingsScriptingInterface(surfaceContext));
surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get<ScriptEngines>().data());
surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get<AvatarBookmarks>().data());
surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get<LocationBookmarks>().data());
@ -3541,7 +3541,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona
surfaceContext->setContextProperty("offscreenFlags", flags);
surfaceContext->setContextProperty("AddressManager", DependencyManager::get<AddressManager>().data());
surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance());
surfaceContext->setContextProperty("Settings", new QMLSettingsScriptingInterface(surfaceContext));
surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance());
surfaceContext->setContextProperty("Performance", new PerformanceScriptingInterface());

View file

@ -725,45 +725,51 @@ Menu::Menu() {
DependencyManager::get<PickManager>().data(), SLOT(setForceCoarsePicking(bool)));
// Developer > Crash >>>
MenuWrapper* crashMenu = developerMenu->addMenu("Crash");
bool result = false;
const QString HIFI_SHOW_DEVELOPER_CRASH_MENU("HIFI_SHOW_DEVELOPER_CRASH_MENU");
result = QProcessEnvironment::systemEnvironment().contains(HIFI_SHOW_DEVELOPER_CRASH_MENU);
if (result) {
MenuWrapper* crashMenu = developerMenu->addMenu("Crash");
// Developer > Crash > Display Crash Options
addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true);
// Developer > Crash > Display Crash Options
addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true);
addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication()));
addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication()));
addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication()));
addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication()));
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction);
connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction);
connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree);
connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree);
connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort);
connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort);
connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference);
connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference);
connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess);
connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess);
connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault);
connect(action, &QAction::triggered, qApp, []() { crash::newFault(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault);
connect(action, &QAction::triggered, qApp, []() { crash::newFault(); });
action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded);
connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); });
addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown()));
addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown()));
}
// Developer > Show Statistics
addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats, 0, true);

View file

@ -38,6 +38,14 @@ void SettingsScriptingInterface::setValue(const QString& setting, const QVariant
if (getValue(setting) == value) {
return;
}
if (setting.startsWith("private/")) {
if (_restrictPrivateValues) {
qWarning() << "SettingsScriptingInterface::setValue -- restricted write: " << setting << value;
return;
} else {
qInfo() << "SettingsScriptingInterface::setValue -- allowing restricted write: " << setting << value;
}
}
// Make a deep-copy of the string.
// Dangling pointers can occur with QStrings that are implicitly shared from a QScriptEngine.
QString deepCopy = QString::fromUtf16(setting.utf16());

View file

@ -27,7 +27,6 @@
class SettingsScriptingInterface : public QObject {
Q_OBJECT
SettingsScriptingInterface() { };
public:
static SettingsScriptingInterface* getInstance();
@ -67,6 +66,16 @@ public slots:
signals:
void valueChanged(const QString& setting, const QVariant& value);
protected:
SettingsScriptingInterface(QObject* parent = nullptr) : QObject(parent) { };
bool _restrictPrivateValues { true };
};
class QMLSettingsScriptingInterface : public SettingsScriptingInterface {
Q_OBJECT
public:
QMLSettingsScriptingInterface(QObject* parent) : SettingsScriptingInterface(parent) { _restrictPrivateValues = false; }
};
#endif // hifi_SettingsScriptingInterface_h

View file

@ -246,6 +246,12 @@ void ModelBaker::bakeSourceCopy() {
// Begin hfm baking
baker.run();
const auto& errors = baker.getDracoErrors();
if (std::find(errors.cbegin(), errors.cend(), true) != errors.cend()) {
handleError("Failed to finalize the baking of a draco Geometry node from model " + _modelURL.toString());
return;
}
_hfmModel = baker.getHFMModel();
_materialMapping = baker.getMaterialMapping();
dracoMeshes = baker.getDracoMeshes();
@ -437,8 +443,7 @@ void ModelBaker::abort() {
bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector<hifi::ByteArray>& dracoMaterialList) {
if (dracoMeshBytes.isEmpty()) {
handleError("Failed to finalize the baking of a draco Geometry node");
return false;
handleWarning("Empty mesh detected in model: '" + _modelURL.toString() + "'. It will be included in the baked output.");
}
FBXNode dracoNode;

View file

@ -28,7 +28,7 @@ protected:
private:
void createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh);
void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel);
void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel);
NodeID nextNodeID() { return _nodeID++; }
NodeID _nodeID { 0 };

View file

@ -29,7 +29,7 @@
#include <PrioritySortUtil.h>
#include <Rig.h>
#include <SceneScriptingInterface.h>
#include <ScriptEngine.h>
#include <ScriptEngines.h>
#include <EntitySimulation.h>
#include <ZoneRenderer.h>
#include <PhysicalEntitySimulation.h>
@ -146,7 +146,7 @@ int EntityTreeRenderer::_entitiesScriptEngineCount = 0;
void EntityTreeRenderer::resetEntitiesScriptEngine() {
_entitiesScriptEngine = scriptEngineFactory(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT,
QString("about:Entities %1").arg(++_entitiesScriptEngineCount));
_scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine);
DependencyManager::get<ScriptEngines>()->runScriptInitializers(_entitiesScriptEngine);
_entitiesScriptEngine->runInThread();
auto entitiesScriptEngineProvider = qSharedPointerCast<EntitiesScriptEngineProvider>(_entitiesScriptEngine);
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();

View file

@ -14,6 +14,7 @@
#include <QUrl>
#include <ResourceManager.h>
#include <shared/ScriptInitializerMixin.h>
QList<EntityItemID> EntityEditFilters::getZonesByPosition(glm::vec3& position) {
QList<EntityItemID> zones;
@ -258,7 +259,13 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) {
if (hasCorrectSyntax(program)) {
// create a QScriptEngine for this script
QScriptEngine* engine = new QScriptEngine();
engine->evaluate(scriptContents);
engine->setObjectName("filter:" + entityID.toString());
engine->setProperty("type", "edit_filter");
engine->setProperty("fileName", urlString);
engine->setProperty("entityID", entityID);
engine->globalObject().setProperty("Script", engine->newQObject(engine));
DependencyManager::get<ScriptInitializers>()->runScriptInitializers(engine);
engine->evaluate(scriptContents, urlString);
if (!hadUncaughtExceptions(*engine, urlString)) {
// put the engine in the engine map (so we don't leak them, etc...)
FilterData filterData;

View file

@ -1075,16 +1075,13 @@ void EntityItem::setMass(float mass) {
void EntityItem::setHref(QString value) {
auto href = value.toLower();
// If the string has something and doesn't start with with "hifi://" it shouldn't be set
// We allow the string to be empty, because that's the initial state of this property
if (!value.isEmpty() &&
!(value.toLower().startsWith("hifi://")) &&
!(value.toLower().startsWith("file://"))
// TODO: serverless-domains will eventually support http and https also
) {
return;
}
// Let's let the user set the value of this property to anything, then let consumers of the property
// decide what to do with it. Currently, the only in-engine consumers are `EntityTreeRenderer::mousePressEvent()`
// and `OtherAvatar::handleChangedAvatarEntityData()` (to remove the href property from others' avatar entities).
//
// We want this property to be as flexible as possible. The value of this property _should_ only be values that can
// be handled by `AddressManager::handleLookupString()`. That function will return `false` and not do
// anything if the value of this property isn't something that function can handle.
withWriteLock([&] {
_href = value;
});

View file

@ -56,7 +56,7 @@ const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
using ShapeVertices = std::vector<glm::vec3>;
// The version of the Draco mesh binary data itself. See also: FBX_DRACO_MESH_VERSION in FBX.h
static const int DRACO_MESH_VERSION = 2;
static const int DRACO_MESH_VERSION = 3;
static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000;
static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES;

View file

@ -120,7 +120,7 @@ namespace baker {
class BakerEngineBuilder {
public:
using Input = VaryingSet3<hfm::Model::Pointer, hifi::VariantHash, hifi::URL>;
using Output = VaryingSet4<hfm::Model::Pointer, MaterialMapping, std::vector<hifi::ByteArray>, std::vector<std::vector<hifi::ByteArray>>>;
using Output = VaryingSet5<hfm::Model::Pointer, MaterialMapping, std::vector<hifi::ByteArray>, std::vector<bool>, std::vector<std::vector<hifi::ByteArray>>>;
using JobModel = Task::ModelIO<BakerEngineBuilder, Input, Output>;
void build(JobModel& model, const Varying& input, Varying& output) {
const auto& hfmModelIn = input.getN<Input>(0);
@ -168,7 +168,8 @@ namespace baker {
const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying();
const auto buildDracoMeshOutputs = model.addJob<BuildDracoMeshTask>("BuildDracoMesh", buildDracoMeshInputs);
const auto dracoMeshes = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(0);
const auto materialList = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(1);
const auto dracoErrors = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(1);
const auto materialList = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(2);
// Parse flow data
const auto flowData = model.addJob<ParseFlowDataTask>("ParseFlowData", mapping);
@ -181,7 +182,7 @@ namespace baker {
const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, flowData).asVarying();
const auto hfmModelOut = model.addJob<BuildModelTask>("BuildModel", buildModelInputs);
output = Output(hfmModelOut, materialMapping, dracoMeshes, materialList);
output = Output(hfmModelOut, materialMapping, dracoMeshes, dracoErrors, materialList);
}
};
@ -212,7 +213,11 @@ namespace baker {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get2();
}
std::vector<std::vector<hifi::ByteArray>> Baker::getDracoMaterialLists() const {
std::vector<bool> Baker::getDracoErrors() const {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get3();
}
std::vector<std::vector<hifi::ByteArray>> Baker::getDracoMaterialLists() const {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get4();
}
};

View file

@ -33,6 +33,7 @@ namespace baker {
hfm::Model::Pointer getHFMModel() const;
MaterialMapping getMaterialMapping() const;
const std::vector<hifi::ByteArray>& getDracoMeshes() const;
std::vector<bool> getDracoErrors() const;
// This is a ByteArray and not a std::string because the character sequence can contain the null character (particularly for FBX materials)
std::vector<std::vector<hifi::ByteArray>> getDracoMaterialLists() const;

View file

@ -51,7 +51,7 @@ std::vector<hifi::ByteArray> createMaterialList(const hfm::Mesh& mesh) {
return materialList;
}
std::unique_ptr<draco::Mesh> createDracoMesh(const hfm::Mesh& mesh, const std::vector<glm::vec3>& normals, const std::vector<glm::vec3>& tangents, const std::vector<hifi::ByteArray>& materialList) {
std::tuple<std::unique_ptr<draco::Mesh>, bool> createDracoMesh(const hfm::Mesh& mesh, const std::vector<glm::vec3>& normals, const std::vector<glm::vec3>& tangents, const std::vector<hifi::ByteArray>& materialList) {
Q_ASSERT(normals.size() == 0 || (int)normals.size() == mesh.vertices.size());
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
@ -68,7 +68,7 @@ std::unique_ptr<draco::Mesh> createDracoMesh(const hfm::Mesh& mesh, const std::v
}
if (numTriangles == 0) {
return std::unique_ptr<draco::Mesh>();
return std::make_tuple(std::unique_ptr<draco::Mesh>(), false);
}
draco::TriangleSoupMeshBuilder meshBuilder;
@ -184,7 +184,7 @@ std::unique_ptr<draco::Mesh> createDracoMesh(const hfm::Mesh& mesh, const std::v
if (!dracoMesh) {
qCWarning(model_baker) << "Failed to finalize the baking of a draco Geometry node";
return std::unique_ptr<draco::Mesh>();
return std::make_tuple(std::unique_ptr<draco::Mesh>(), true);
}
// we need to modify unique attribute IDs for custom attributes
@ -201,7 +201,7 @@ std::unique_ptr<draco::Mesh> createDracoMesh(const hfm::Mesh& mesh, const std::v
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
}
return dracoMesh;
return std::make_tuple(std::move(dracoMesh), false);
}
#endif // not Q_OS_ANDROID
@ -218,9 +218,13 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp
const auto& normalsPerMesh = input.get1();
const auto& tangentsPerMesh = input.get2();
auto& dracoBytesPerMesh = output.edit0();
auto& materialLists = output.edit1();
auto& dracoErrorsPerMesh = output.edit1();
auto& materialLists = output.edit2();
dracoBytesPerMesh.reserve(meshes.size());
// vector<bool> is an exception to the std::vector conventions as it is a bit field
// So a bool reference to an element doesn't work
dracoErrorsPerMesh.resize(meshes.size());
materialLists.reserve(meshes.size());
for (size_t i = 0; i < meshes.size(); i++) {
const auto& mesh = meshes[i];
@ -231,7 +235,10 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp
materialLists.push_back(createMaterialList(mesh));
const auto& materialList = materialLists.back();
auto dracoMesh = createDracoMesh(mesh, normals, tangents, materialList);
bool dracoError;
std::unique_ptr<draco::Mesh> dracoMesh;
std::tie(dracoMesh, dracoError) = createDracoMesh(mesh, normals, tangents, materialList);
dracoErrorsPerMesh[i] = dracoError;
if (dracoMesh) {
draco::Encoder encoder;

View file

@ -34,7 +34,7 @@ class BuildDracoMeshTask {
public:
using Config = BuildDracoMeshConfig;
using Input = baker::VaryingSet3<std::vector<hfm::Mesh>, baker::NormalsPerMesh, baker::TangentsPerMesh>;
using Output = baker::VaryingSet2<std::vector<hifi::ByteArray>, std::vector<std::vector<hifi::ByteArray>>>;
using Output = baker::VaryingSet3<std::vector<hifi::ByteArray>, std::vector<bool>, std::vector<std::vector<hifi::ByteArray>>>;
using JobModel = baker::Job::ModelIO<BuildDracoMeshTask, Input, Output, Config>;
void configure(const Config& config);

View file

@ -85,6 +85,13 @@ namespace {
const QString& CACHE_ERROR_MESSAGE{ "AssetClient::Error: %1 %2" };
}
/**jsdoc
* Cache status value returned by {@link Assets.getCacheStatus}.
* @typedef {object} Assets.GetCacheStatusResult
* @property {string} cacheDirectory - The path of the cache directory.
* @property {number} cacheSize - The current cache size, in bytes.
* @property {number} maximumCacheSize - The maximum cache size, in bytes.
*/
MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise deferred) {
if (!deferred) {
deferred = makePromise(__FUNCTION__); // create on caller's thread
@ -106,6 +113,20 @@ MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise def
return deferred;
}
/**jsdoc
* Information on an asset in the cache. Value returned by {@link Assets.queryCacheMeta} and included in the data returned by
* {@link Assets.loadFromCache}.
* @typedef {object} Assets.CacheItemMetaData
* @property {object} [attributes] - The attributes that are stored with this cache item. <em>Not used.</em>
* @property {Date} [expirationDate] - The date and time when the meta data expires. An invalid date means "never expires".
* @property {boolean} isValid - <code>true</code> if the item specified in the URL is in the cache, <code>false</code> if
* it isn't.
* @property {Date} [lastModified] - The date and time when the meta data was last modified.
* @property {object} [rawHeaders] - The raw headers that are set in the meta data. <em>Not used.</em>
* @property {boolean} [saveToDisk] - <code>true</code> if the cache item is allowed to be store on disk,
* <code>false</code> if it isn't.
* @property {string} [url|metaDataURL] - The ATP URL of the cached item.
*/
MiniPromise::Promise AssetClient::queryCacheMetaAsync(const QUrl& url, MiniPromise::Promise deferred) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "queryCacheMetaAsync", Q_ARG(const QUrl&, url), Q_ARG(MiniPromise::Promise, deferred));
@ -202,6 +223,24 @@ namespace {
}
}
/**jsdoc
* Last-modified and expiry times for a cache item.
* @typedef {object} Assets.SaveToCacheHeaders
* @property {string} [expires] - The date and time the cache value expires, in the format:
* <code>"ddd, dd MMM yyyy HH:mm:ss"</code>. The default value is an invalid date, representing "never expires".
* @property {string} [last-modified] - The date and time the cache value was last modified, in the format:
* <code>"ddd, dd MMM yyyy HH:mm:ss"</code>. The default value is the current date and time.
*/
/**jsdoc
* Information on saving asset data to the cache with {@link Assets.saveToCache}.
* @typedef {object} Assets.SaveToCacheResult
* @property {number} [byteLength] - The size of the cached data, in bytes.
* @property {Date} [expirationDate] - The date and time that the cache item expires. An invalid date means "never expires".
* @property {Date} [lastModified] - The date and time that the cache item was last modified.
* @property {string} [metaDataURL] - The URL associated with the cache item.
* @property {boolean} [success] - <code>true</code> if the save to cache request was successful.
* @property {string} [url] - The URL associated with the cache item.
*/
MiniPromise::Promise AssetClient::saveToCacheAsync(const QUrl& url, const QByteArray& data, const QVariantMap& headers, MiniPromise::Promise deferred) {
if (!deferred) {
deferred = makePromise(__FUNCTION__); // create on caller's thread

View file

@ -154,7 +154,7 @@ const char* Assignment::typeToString(Assignment::Type type) {
QDebug operator<<(QDebug debug, const Assignment &assignment) {
debug.nospace() << "UUID: " << qPrintable(assignment.getUUID().toString()) <<
", Type: " << assignment.getType();
", Type: " << assignment.getTypeName() << " (" << assignment.getType() << ")";
if (!assignment.getPool().isEmpty()) {
debug << ", Pool: " << assignment.getPool();

View file

@ -68,6 +68,17 @@ Promise BaseAssetScriptingInterface::queryCacheMeta(const QUrl& url) {
return assetClient()->queryCacheMetaAsync(url, makePromise(__FUNCTION__));
}
/**jsdoc
* Data and information returned by {@link Assets.loadFromCache}.
* @typedef {object} Assets.LoadFromCacheResult
* @property {number} [byteLength] - The number of bytes in the retrieved data.
* @property {string} [contentType] - The automatically detected MIME type of the content.
* @property {ArrayBuffer} data - The data bytes.
* @property {Assets.CacheItemMetaData} metadata - Information on the cache item.
* @property {string|object|ArrayBuffer} [response] - The content of the response.
* @property {Assets.ResponseType} responseType - The type of the content in <code>response</code>.
* @property {string} url - The URL of the cache item.
*/
Promise BaseAssetScriptingInterface::loadFromCache(const QUrl& url, bool decompress, const QString& responseType) {
QVariantMap metaData = {
{ "_type", "cache" },

View file

@ -24,6 +24,22 @@
class BaseAssetScriptingInterface : public QObject {
Q_OBJECT
public:
/**jsdoc
* <p>Types of response that {@link Assets.decompressData}, {@link Assets.getAsset}, or {@link Assets.loadFromCache} may
* provide.</p>
* <table>
* <thead>
* <tr><th>Value</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td><code>"arraybuffer"</code></td><td>A binary <code>ArrayBuffer</code> object.</td></tr>
* <tr><td><code>"json"</code></td><td>A parsed <code>JSON</code> object.</td></tr>
* <tr><td><code>"text"</code></td><td>UTF-8 decoded <code>string</code> value.</td></tr>
* </tbody>
* </table>
* @typedef {string} Assets.ResponseType
*/
const QStringList RESPONSE_TYPES{ "text", "arraybuffer", "json" };
using Promise = MiniPromise::Promise;
QSharedPointer<AssetClient> assetClient();
@ -33,51 +49,62 @@ public:
public slots:
/**jsdoc
* Checks whether a string is a valid path. Note: A valid path must start with a <code>"/"</code>.
* @function Assets.isValidPath
* @param {string} input
* @returns {boolean}
* @param {string} path - The path to check.
* @returns {boolean} <code>true</code> if the path is a valid path, <code>false</code> if it isn't.
*/
bool isValidPath(QString input) { return AssetUtils::isValidPath(input); }
/**jsdoc
* Checks whether a string is a valid path and filename. Note: A valid path and filename must start with a <code>"/"</code>
* but must not end with a <code>"/"</code>.
* @function Assets.isValidFilePath
* @param {string} input
* @returns {boolean}
* @param {string} path - The path to check.
* @returns {boolean} <code>true</code> if the path is a valid file path, <code>false</code> if it isn't.
*/
bool isValidFilePath(QString input) { return AssetUtils::isValidFilePath(input); }
/**jsdoc
* Gets the normalized ATP URL for a path or hash: ensures that it has <code>"atp:"</code> at the start.
* @function Assets.getATPUrl
* @param {string} input
* @returns {string}
* @param {string} url - The URL to normalize.
* @returns {string} The normalized ATP URL.
*/
QUrl getATPUrl(QString input) { return AssetUtils::getATPUrl(input); }
/**jsdoc
* Gets the SHA256 hexadecimal hash portion of an asset server URL.
* @function Assets.extractAssetHash
* @param {string} input
* @returns {string}
* @param {string} url - The URL to get the SHA256 hexadecimal hash from.
* @returns {string} The SHA256 hexadecimal hash portion of the URL if present and valid, <code>""</code> otherwise.
*/
QString extractAssetHash(QString input) { return AssetUtils::extractAssetHash(input); }
/**jsdoc
* Checks whether a string is a valid SHA256 hexadecimal hash, i.e., 64 hexadecimal characters.
* @function Assets.isValidHash
* @param {string} input
* @returns {boolean}
* @param {string} hash - The hash to check.
* @returns {boolean} <code>true</code> if the hash is a valid SHA256 hexadecimal string, <code>false</code> if it isn't.
*/
bool isValidHash(QString input) { return AssetUtils::isValidHash(input); }
/**jsdoc
* Calculates the SHA256 hash of given data.
* @function Assets.hashData
* @param {} data
* @returns {object}
* @param {string|ArrayBuffer} data - The data to calculate the hash of.
* @returns {ArrayBuffer} The SHA256 hash of the <code>data</code>.
*/
QByteArray hashData(const QByteArray& data) { return AssetUtils::hashData(data); }
/**jsdoc
* Calculates the SHA256 hash of given data, in hexadecimal format.
* @function Assets.hashDataHex
* @param {} data
* @returns {string}
* @param {string|ArrayBuffer} data - The data to calculate the hash of.
* @returns {string} The SHA256 hash of the <code>data</code>, in hexadecimal format.
* @example <caption>Calculate the hash of some text.</caption>
* var text = "Hello world!";
* print("Hash: " + Assets.hashDataHex(text));
*/
QString hashDataHex(const QByteArray& data) { return hashData(data).toHex(); }

View file

@ -29,6 +29,9 @@ ThreadedAssignment::ThreadedAssignment(ReceivedMessage& message) :
_domainServerTimer(this),
_statsTimer(this)
{
// use <mixer-type> as a temporary targetName name until commonInit can be called later
LogHandler::getInstance().setTargetName(QString("<%1>").arg(getTypeName()));
static const int STATS_TIMEOUT_MS = 1000;
_statsTimer.setInterval(STATS_TIMEOUT_MS); // 1s, Qt::CoarseTimer acceptable
connect(&_statsTimer, &QTimer::timeout, this, &ThreadedAssignment::sendStatsPacket);

View file

@ -546,7 +546,6 @@ void Socket::handleStateChanged(QAbstractSocket::SocketState socketState) {
void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAddr currentAddress) {
{
Lock connectionsLock(_connectionsHashMutex);
_connectionsHash.erase(currentAddress);
const auto connectionIter = _connectionsHash.find(previousAddress);
if (connectionIter != _connectionsHash.end()) {
@ -554,18 +553,16 @@ void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAdd
_connectionsHash.erase(connectionIter);
connection->setDestinationAddress(currentAddress);
_connectionsHash[currentAddress] = move(connection);
}
}
connectionsLock.unlock();
{
Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex);
_unreliableSequenceNumbers.erase(currentAddress);
Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex);
const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress);
if (sequenceNumbersIter != _unreliableSequenceNumbers.end()) {
auto sequenceNumbers = sequenceNumbersIter->second;
_unreliableSequenceNumbers.erase(sequenceNumbersIter);
_unreliableSequenceNumbers[currentAddress] = sequenceNumbers;
}
const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress);
if (sequenceNumbersIter != _unreliableSequenceNumbers.end()) {
auto sequenceNumbers = sequenceNumbersIter->second;
_unreliableSequenceNumbers.erase(sequenceNumbersIter);
_unreliableSequenceNumbers[currentAddress] = sequenceNumbers;
}
}
}

View file

@ -84,6 +84,11 @@ bool isDisabled(QJsonObject metaData) {
return false;
}
int PluginManager::instantiate() {
auto loaders = getLoadedPlugins();
return std::count_if(loaders.begin(), loaders.end(), [](const auto& loader) { return (bool)loader->instance(); });
}
auto PluginManager::getLoadedPlugins() const -> const LoaderList& {
static std::once_flag once;
static LoaderList loadedPlugins;
@ -105,6 +110,16 @@ bool isDisabled(QJsonObject metaData) {
pluginDir.setNameFilters(QStringList() << "libplugins_lib*.so");
#endif
auto candidates = pluginDir.entryList();
if (_enableScriptingPlugins.get()) {
QDir scriptingPluginDir{ pluginDir };
scriptingPluginDir.cd("scripting");
qCDebug(plugins) << "Loading scripting plugins from " << scriptingPluginDir.path();
for (auto plugin : scriptingPluginDir.entryList()) {
candidates << "scripting/" + plugin;
}
}
for (auto plugin : candidates) {
qCDebug(plugins) << "Attempting plugin" << qPrintable(plugin);
QSharedPointer<QPluginLoader> loader(new QPluginLoader(pluginPath + plugin));
@ -139,6 +154,8 @@ bool isDisabled(QJsonObject metaData) {
qCDebug(plugins) << " " << qPrintable(loader->errorString());
}
}
} else {
qWarning() << "pluginPath does not exit..." << pluginDir;
}
});
return loadedPlugins;

View file

@ -10,6 +10,7 @@
#include <QObject>
#include <DependencyManager.h>
#include <SettingHandle.h>
#include "Forward.h"
@ -38,6 +39,7 @@ public:
void saveSettings();
void setContainer(PluginContainer* container) { _container = container; }
int instantiate();
void shutdown();
// Application that have statically linked plugins can expose them to the plugin manager with these function
@ -69,6 +71,9 @@ private:
using LoaderList = QList<Loader>;
const LoaderList& getLoadedPlugins() const;
Setting::Handle<bool> _enableScriptingPlugins {
"private/enableScriptingPlugins", (bool)qgetenv("enableScriptingPlugins").toInt()
};
};
// TODO: we should define this value in CMake, and then use CMake

View file

@ -81,6 +81,11 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu
setMappingRequest->start();
}
/**jsdoc
* The success or failure of an {@link Assets.downloadData} call.
* @typedef {object} Assets.DownloadDataError
* @property {string} errorMessage - <code>""</code> if the download was successful, otherwise a description of the error.
*/
void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) {
// FIXME: historically this API method failed silently when given a non-atp prefixed
// urlString (or if the AssetRequest failed).
@ -219,20 +224,31 @@ void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue sco
}
/**jsdoc
* @typedef {string} Assets.GetOptions.ResponseType
* <p>Available <code>responseType</code> values for use with @{link Assets.getAsset} and @{link Assets.loadFromCache} configuration option. </p>
* <table>
* <thead>
* <tr><th>responseType</th><th>typeof response value</th></tr>
* </thead>
* <tbody>
* <tr><td><code>"text"</code></td><td>contents returned as utf-8 decoded <code>String</code> value</td></tr>
* <tr><td><code>"arraybuffer"</code></td><td>contents as a binary <code>ArrayBuffer</code> object</td></tr>
* <tr><td><code>"json"</code></td><td>contents as a parsed <code>JSON</code> object</td></tr>
* </tbody>
* </table>
* Source and download options for {@link Assets.getAsset}.
* @typedef {object} Assets.GetOptions
* @property {boolean} [decompress=false] - <code>true</code> to gunzip decompress the downloaded data. Synonym:
* <code>compressed</code>.
* @property {Assets.ResponseType} [responseType="text"] - The desired result type.
* @property {string} url - The mapped path or hash to download. May have a leading <code>"atp:"</code>.
*/
/**jsdoc
* Result value returned by {@link Assets.getAsset}.
* @typedef {object} Assets.GetResult
* @property {number} [byteLength] - The number of bytes in the downloaded content in <code>response</code>.
* @property {boolean} cached - <code>true</code> if the item was retrieved from the cache, <code>false</code> if it was
* downloaded.
* @property {string} [contentType] - The automatically detected MIME type of the content.
* @property {boolean} [decompressed] - <code>true</code> if the content was decompressed, <code>false</code> if it wasn't.
* @property {string} [hash] - The hash for the downloaded asset.
* @property {string} [hashURL] - The ATP URL of the hash file.
* @property {string} [path] - The path for the asset, if a path was requested. Otherwise, <code>undefined</code>.
* @property {string|object|ArrayBuffer} [response] - The downloaded content.
* @property {Assets.ResponseType} [responseType] - The type of the downloaded content in <code>response</code>.
* @property {string} [url] - The URL of the asset requested: the path with leading <code>"atp:"</code> if a path was
* requested, otherwise the requested URL.
* @property {boolean} [wasRedirected] - <code>true</code> if the downloaded data is the baked version of the asset,
* <code>false</code> if it isn't baked.
*/
void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
JS_VERIFY(options.isObject() || options.isString(), "expected request options Object or URL as first parameter");
@ -283,6 +299,22 @@ void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope,
}
}
/**jsdoc
* Source options for {@link Assets.resolveAsset}.
* @typedef {object} Assets.ResolveOptions
* @property {string} url - The hash or path to resolve. May have a leading <code>"atp:"</code>.
*/
/**jsdoc
* Result value returned by {@link Assets.resolveAsset}.
* <p>Note: If resolving a hash, a file of that hash need not be present on the asset server for the hash to resolve.</p>
* @typedef {object} Assets.ResolveResult
* @property {string} [hash] - The hash of the asset.
* @property {string} [hashURL] - The url of the asset's hash file, with leading <code>atp:</code>.
* @property {string} [path] - The path to the asset.
* @property {string} [url] - The URL of the asset.
* @property {boolean} [wasRedirected] - <code>true</code> if the resolved data is for the baked version of the asset,
* <code>false</code> if it isn't.
*/
void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
const QString& URL{ "url" };
@ -295,6 +327,21 @@ void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue sc
jsPromiseReady(getAssetInfo(asset), scope, callback);
}
/**jsdoc
* Content and decompression options for {@link Assets.decompressData}.
* @typedef {object} Assets.DecompressOptions
* @property {ArrayBuffer} data - The data to decompress.
* @property {Assets.ResponseType} [responseType=text] - The type of decompressed data to return.
*/
/**jsdoc
* Result value returned by {@link Assets.decompressData}.
* @typedef {object} Assets.DecompressResult
* @property {number} [byteLength] - The number of bytes in the decompressed data.
* @property {string} [contentType] - The MIME type of the decompressed data.
* @property {boolean} [decompressed] - <code>true</code> if the data is decompressed.
* @property {string|object|ArrayBuffer} [response] - The decompressed data.
* @property {Assets.ResponseType} [responseType] - The type of the decompressed data in <code>response</code>.
*/
void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) {
auto data = options.property("data");
QByteArray dataByteArray = qscriptvalue_cast<QByteArray>(data);
@ -319,6 +366,23 @@ namespace {
const int32_t DEFAULT_GZIP_COMPRESSION_LEVEL = -1;
const int32_t MAX_GZIP_COMPRESSION_LEVEL = 9;
}
/**jsdoc
* Content and compression options for {@link Assets.compressData}.
* @typedef {object} Assets.CompressOptions
* @property {string|ArrayBuffer} data - The data to compress.
* @property {number} level - The compression level, range <code>-1</code> &ndash; <code>9</code>. <code>-1</code> means
* use the default gzip compression level, <code>0</code> means no compression, and <code>9</code> means maximum
* compression.
*/
/**jsdoc
* Result value returned by {@link Assets.compressData}.
* @typedef {object} Assets.CompressResult
* @property {number} [byteLength] - The number of bytes in the compressed data.
* @property {boolean} [compressed] - <code>true</code> if the data is compressed.
* @property {string} [contentType] - The MIME type of the compressed data, i.e., <code>"application/gzip"</code>.
* @property {ArrayBuffer} [data] - The compressed data.
*/
void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue scope, QScriptValue callback) {
auto data = options.property("data").isValid() ? options.property("data") : options;
QByteArray dataByteArray = data.isString() ? data.toString().toUtf8() : qscriptvalue_cast<QByteArray>(data);
@ -327,6 +391,27 @@ void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue sc
jsPromiseReady(compressBytes(dataByteArray, level), scope, callback);
}
/**jsdoc
* Content and upload options for {@link Assets.putAsset}.
* @typedef {object} Assets.PutOptions
* @property {boolean} [compress=false] - <code>true</code> to gzip compress the content for upload and storage,
* <code>false</code> to upload and store the data without gzip compression. Synonym: <code>compressed</code>.
* @property {string|ArrayBuffer} data - The content to upload.
* @property {string} [path] - A user-friendly path for the file in the asset server. May have a leading
* <code>"atp:"</code>. IF not specified, no path-to-hash mapping is set.
* <p>Note: The asset server destroys any unmapped SHA256-named file at server restart. Either set the mapping path
* with this property or use {@link Assets.setMapping} to set a path-to-hash mapping for the uploaded file.</p>
*/
/**jsdoc
* Result value returned by {@link Assets.putAsset}.
* @typedef {object} Assets.PutResult
* @property {number} [byteLength] - The number of bytes in the hash file stored on the asset server.
* @property {boolean} [compressed] - <code>true</code> if the content stored is gzip compressed.
* @property {string} [contentType] - <code>"application/gzip"</code> if the content stored is gzip compressed.
* @property {string} [hash] - The SHA256 hash of the content.
* @property {string} [url] - The <code>atp:</code> URL of the content: using the path if specified, otherwise the hash.
* @property {string} [path] - The uploaded content's mapped path, if specified.
*/
void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
auto compress = options.property("compress").toBool() || options.property("compressed").toBool();
auto data = options.isObject() ? options.property("data") : options;
@ -377,12 +462,27 @@ void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope,
}
}
/**jsdoc
* Source for {@link Assets.queryCacheMeta}.
* @typedef {object} Assets.QueryCacheMetaOptions
* @property {string} url - The URL of the cached asset to get information on. Must start with <code>"atp:"</code> or
* <code>"cache:"</code>.
*/
void AssetScriptingInterface::queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback) {
QString url = options.isString() ? options.toString() : options.property("url").toString();
JS_VERIFY(QUrl(url).isValid(), QString("Invalid URL '%1'").arg(url));
jsPromiseReady(Parent::queryCacheMeta(url), scope, callback);
}
/**jsdoc
* Source and retrieval options for {@link Assets.loadFromCache}.
* @typedef {object} Assets.LoadFromCacheOptions
* @property {boolean} [decompress=false] - <code>true</code> to gunzip decompress the cached data. Synonym:
* <code>compressed</code>.
* @property {Assets.ResponseType} [responseType=text] - The desired result type.
* @property {string} url - The URL of the asset to load from cache. Must start with <code>"atp:"</code> or
* <code>"cache:"</code>.
*/
void AssetScriptingInterface::loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback) {
QString url, responseType;
bool decompress = false;
@ -417,6 +517,14 @@ bool AssetScriptingInterface::canWriteCacheValue(const QUrl& url) {
return true;
}
/**jsdoc
* The data to save to the cache and cache options for {@link Assets.saveToCache}.
* @typedef {object} Assets.SaveToCacheOptions
* @property {string|ArrayBuffer} data - The data to save to the cache.
* @property {Assets.SaveToCacheHeaders} [headers] - The last-modified and expiry times for the cache item.
* @property {string} [url] - The URL to associate with the cache item. Must start with <code>"atp:"</code> or
* <code>"cache:"</code>. If not specified, the URL is <code>"atp:"</code> followed by the SHA256 hash of the content.
*/
void AssetScriptingInterface::saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback) {
JS_VERIFY(options.isObject(), QString("expected options object as first parameter not: %1").arg(options.toVariant().typeName()));

View file

@ -25,7 +25,14 @@
#include <QtNetwork/QNetworkDiskCache>
/**jsdoc
* The Assets API allows you to communicate with the Asset Browser.
* The <code>Assets</code> API provides facilities for interacting with the domain's asset server and the client cache.
* <p>Assets are stored in the asset server in files with SHA256 names. These files are mapped to user-friendly URLs of the
* format: <code>atp:/path/filename</code>. The assets may optionally be baked, in which case a request for the original
* unbaked version of the asset is automatically redirected to the baked version. The asset data may optionally be stored as
* compressed.</p>
* <p>The client cache can be access directly, using <code>"atp:"</code> or <code>"cache:"</code> URLs. Interface, avatar, and
* assignment client scripts can write to the cache. All script types can read from the cache.</p>
*
* @namespace Assets
*
* @hifi-interface
@ -41,251 +48,490 @@ public:
AssetScriptingInterface(QObject* parent = nullptr);
/**jsdoc
* Upload content to the connected domain's asset server.
* @function Assets.uploadData
* @static
* @param data {string} content to upload
* @param callback {Assets~uploadDataCallback} called when upload is complete
* Called when an {@link Assets.uploadData} call is complete.
* @callback Assets~uploadDataCallback
* @param {string} url - The raw URL of the file that the content is stored in, with <code>atp:</code> as the scheme and
* the SHA256 hash as the filename (with no extension).
* @param {string} hash - The SHA256 hash of the content.
*/
/**jsdoc
* Called when uploadData is complete
* @callback Assets~uploadDataCallback
* @param {string} url
* @param {string} hash
* Uploads content to the asset server, storing it in a SHA256-named file.
* <p>Note: The asset server destroys any unmapped SHA256-named file at server restart. Use {@link Assets.setMapping} to
* set a path-to-hash mapping for the new file.</p>
* @function Assets.uploadData
* @param {string} data - The content to upload.
* @param {Assets~uploadDataCallback} callback - The function to call upon completion.
* @example <caption>Store a string in the asset server.</caption>
* Assets.uploadData("Hello world!", function (url, hash) {
* print("URL: " + url); // atp:0a1b...9g
* Assets.setMapping("/assetsExamples/helloWorld.txt", hash, function (error) {
* if (error) {
* print("ERROR: Could not set mapping!");
* return;
* }
* });
* });
*/
Q_INVOKABLE void uploadData(QString data, QScriptValue callback);
/**jsdoc
* Download data from the connected domain's asset server.
* @function Assets.downloadData
* @param url {string} URL of asset to download, must be ATP scheme URL.
* @param callback {Assets~downloadDataCallback}
* Called when an {@link Assets.downloadData} call is complete.
* @callback Assets~downloadDataCallback
* @param {string} data - The content that was downloaded.
* @param {Assets.DownloadDataError} error - The success or failure of the download.
*/
/**jsdoc
* Called when downloadData is complete
* @callback Assets~downloadDataCallback
* @param data {string} content that was downloaded
* Downloads content from the asset server, from a SHA256-named file.
* @function Assets.downloadData
* @param {string} url - The raw URL of asset to download: <code>atp:</code> followed by the assets's SHA256 hash.
* @param {Assets~downloadDataCallback} callback - The function to call upon completion.
* @example <caption>Store and retrieve a string from the asset server.</caption>
* var assetURL;
*
* // Store the string.
* Assets.uploadData("Hello world!", function (url, hash) {
* assetURL = url;
* print("url: " + assetURL); // atp:a0g89...
* Assets.setMapping("/assetsExamples/helloWorld.txt", hash, function (error) {
* if (error) {
* print("ERROR: Could not set mapping!");
* return;
* }
* });
* });
*
* // Retrieve the string.
* Script.setTimeout(function () {
* Assets.downloadData(assetURL, function (data, error) {
* print("Downloaded data: " + data);
* print("Error: " + JSON.stringify(error));
* });
* }, 1000);
*/
Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete);
Q_INVOKABLE void downloadData(QString url, QScriptValue callback);
/**jsdoc
* Sets up a path to hash mapping within the connected domain's asset server
* @function Assets.setMapping
* @param path {string}
* @param hash {string}
* @param callback {Assets~setMappingCallback}
* Called when an {@link Assets.setMapping} call is complete.
* @callback Assets~setMappingCallback
* @param {string} error - <code>null</code> if the path-to-hash mapping was set, otherwise a description of the error.
*/
/**jsdoc
* Called when setMapping is complete
* @callback Assets~setMappingCallback
* @param {string} error
* Sets a path-to-hash mapping within the asset server.
* @function Assets.setMapping
* @param {string} path - A user-friendly path for the file in the asset server, without leading <code>"atp:"</code>.
* @param {string} hash - The hash in the asset server.
* @param {Assets~setMappingCallback} callback - The function to call upon completion.
*/
Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback);
/**jsdoc
* Look up a path to hash mapping within the connected domain's asset server
* @function Assets.getMapping
* @param path {string}
* @param callback {Assets~getMappingCallback}
* Called when an {@link Assets.getMapping} call is complete.
* @callback Assets~getMappingCallback
* @param {string} error - <code>null</code> if the path was found, otherwise a description of the error.
* @param {string} hash - The hash value if the path was found, <code>""</code> if it wasn't.
*/
/**jsdoc
* Called when getMapping is complete.
* @callback Assets~getMappingCallback
* @param assetID {string} hash value if found, else an empty string
* @param error {string} error description if the path could not be resolved; otherwise a null value.
* Gets the hash for a path within the asset server. The hash is for the unbaked or baked version of the
* asset, according to the asset server setting for the particular path.
* @function Assets.getMapping
* @param {string} path - The path to a file in the asset server to get the hash of.
* @param {Assets~getMappingCallback} callback - The function to call upon completion.
* @example <caption>Report the hash of an asset server item.</caption>
* var assetPath = Window.browseAssets();
* if (assetPath) {
* var mapping = Assets.getMapping(assetPath, function (error, hash) {
* print("Asset: " + assetPath);
* print("- hash: " + hash);
* print("- error: " + error);
* });
* }
*/
Q_INVOKABLE void getMapping(QString path, QScriptValue callback);
/**jsdoc
* @function Assets.setBakingEnabled
* @param path {string}
* @param enabled {boolean}
* @param callback {}
* Called when an {@link Assets.setBakingEnabled} call is complete.
* @callback Assets~setBakingEnabledCallback
* @param {string} error - <code>null</code> if baking was successfully enabled or disabled, otherwise a description of the
* error.
*/
/**jsdoc
* Called when setBakingEnabled is complete.
* @callback Assets~setBakingEnabledCallback
* Sets whether or not to bake an asset in the asset server.
* @function Assets.setBakingEnabled
* @param {string} path - The path to a file in the asset server.
* @param {boolean} enabled - <code>true</code> to enable baking of the asset, <code>false</code> to disable.
* @param {Assets~setBakingEnabledCallback} callback - The function to call upon completion.
*/
// Note: Second callback parameter not documented because it's always {}.
Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback);
#if (PR_BUILD || DEV_BUILD)
/**
* This function is purely for development purposes, and not meant for use in a
* production context. It is not a public-facing API, so it should not contain jsdoc.
* production context. It is not a public-facing API, so it should not have JSDoc.
*/
Q_INVOKABLE void sendFakedHandshake();
#endif
/**jsdoc
* Request Asset data from the ATP Server
* @function Assets.getAsset
* @param {URL|Assets.GetOptions} options An atp: style URL, hash, or relative mapped path; or an {@link Assets.GetOptions} object with request parameters
* @param {Assets~getAssetCallback} scope A scope callback function to receive (error, results) values
* @param {function} [callback=undefined]
* Details of a callback function.
* @typedef {object} Assets.CallbackDetails
* @property {object} scope - The scope that the <code>callback</code> function is defined in. This object is bound to
* <code>this</code> when the function is called.
* @property {Assets~compressDataCallback|Assets~decompressDataCallback|Assets~getAssetCallback
* |Assets~getCacheStatusCallback|Assets~loadFromCacheCallback|Assets~putAssetCallback|Assets~queryCacheMetaCallback
* |Assets~resolveAssetCallback|Assets~saveToCacheCallback}
* callback - The function to call upon completion. May be an inline function or a function identifier. If a function
* identifier, it must be a member of <code>scope</code>.
*/
/**jsdoc
* A set of properties that can be passed to {@link Assets.getAsset}.
* @typedef {object} Assets.GetOptions
* @property {string} [url] an "atp:" style URL, hash, or relative mapped path to fetch
* @property {string} [responseType=text] the desired reponse type (text | arraybuffer | json)
* @property {boolean} [decompress=false] whether to attempt gunzip decompression on the fetched data
* See: {@link Assets.putAsset} and its .compress=true option
*/
/**jsdoc
* Called when Assets.getAsset is complete.
* Called when an {@link Assets.getAsset} call is complete.
* @callback Assets~getAssetCallback
* @param {string} error - contains error message or null value if no error occured fetching the asset
* @param {Asset~getAssetResult} result - result object containing, on success containing asset metadata and contents
* @param {string} error - <code>null</code> if the content was downloaded, otherwise a description of the error.
* @param {Assets.GetResult} result - Information on and the content downloaded.
*/
/**jsdoc
* Result value returned by {@link Assets.getAsset}.
* @typedef {object} Assets~getAssetResult
* @property {string} [url] the resolved "atp:" style URL for the fetched asset
* @property {string} [hash] the resolved hash for the fetched asset
* @property {string|ArrayBuffer|Object} [response] response data (possibly converted per .responseType value)
* @property {string} [responseType] response type (text | arraybuffer | json)
* @property {string} [contentType] detected asset mime-type (autodetected)
* @property {number} [byteLength] response data size in bytes
* @property {number} [decompressed] flag indicating whether data was decompressed
* Downloads content from the asset server.
* @function Assets.getAsset
* @param {string|Assets.GetOptions} source - What to download and download options. If a string, the mapped path or hash
* to download, optionally including a leading <code>"atp:"</code>.
* @param {object|Assets.CallbackDetails|Assets~getAssetCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~getAssetCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
* @example <caption>Retrieve a string from the asset server.</caption>
* Assets.getAsset(
* {
* url: "/assetsExamples/helloWorld.txt",
* responseType: "text"
* },
* function (error, result) {
* if (error) {
* print("ERROR: Data not downloaded");
* } else {
* print("Data: " + result.response);
* }
* }
* );
*/
Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* Upload Asset data to the ATP Server
* Called when an {@link Assets.putAsset} call is complete.
* @callback Assets~putAssetCallback
* @param {string} error - <code>null</code> if the content was uploaded and any path-to-hash mapping set, otherwise a
* description of the error.
* @param {Assets.PutResult} result - Information on the content uploaded.
*/
/**jsdoc
* Uploads content to the asset server and sets a path-to-hash mapping.
* @function Assets.putAsset
* @param {Assets.PutOptions} options A PutOptions object with upload parameters
* @param {Assets~putAssetCallback} scope[callback] A scoped callback function invoked with (error, results)
* @param {function} [callback=undefined]
* @param {string|Assets.PutOptions} options - The content to upload and upload options. If a string, the value of the
* string is uploaded but a path-to-hash mapping is not set.
* @param {object|Assets.CallbackDetails|Assets~putAssetCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~putAssetCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
* @example <caption>Store a string in the asset server.</caption>
* Assets.putAsset(
* {
* data: "Hello world!",
* path: "/assetsExamples/helloWorld.txt"
* },
* function (error, result) {
* if (error) {
* print("ERROR: Data not uploaded or mapping not set");
* } else {
* print("URL: " + result.url); // atp:/assetsExamples/helloWorld.txt
* }
* }
* );
*/
/**jsdoc
* A set of properties that can be passed to {@link Assets.putAsset}.
* @typedef {object} Assets.PutOptions
* @property {ArrayBuffer|string} [data] byte buffer or string value representing the new asset's content
* @property {string} [path=null] ATP path mapping to automatically create (upon successful upload to hash)
* @property {boolean} [compress=false] whether to gzip compress data before uploading
*/
/**jsdoc
* Called when Assets.putAsset is complete.
* @callback Assets~puttAssetCallback
* @param {string} error - contains error message (or null value if no error occured while uploading/mapping the new asset)
* @param {Asset~putAssetResult} result - result object containing error or result status of asset upload
*/
/**jsdoc
* Result value returned by {@link Assets.putAsset}.
* @typedef {object} Assets~putAssetResult
* @property {string} [url] the resolved "atp:" style URL for the uploaded asset (based on .path if specified, otherwise on the resulting ATP hash)
* @property {string} [path] the uploaded asset's resulting ATP path (or undefined if no path mapping was assigned)
* @property {string} [hash] the uploaded asset's resulting ATP hash
* @property {boolean} [compressed] flag indicating whether the data was compressed before upload
* @property {number} [byteLength] flag indicating final byte size of the data uploaded to the ATP server
*/
Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* @function Assets.deleteAsset
* @param {} options
* @param {} scope
* @param {} [callback = ""]
* Called when an {@link Assets.deleteAsset} call is complete.
* <p class="important">Not implemented: This type is not implemented yet.</p>
* @callback Assets~deleteAssetCallback
* @param {string} error - <code>null</code> if the content was deleted, otherwise a description of the error.
* @param {Assets.DeleteResult} result - Information on the content deleted.
*/
Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* @function Assets.resolveAsset
* @param {} options
* @param {} scope
* @param {} [callback = ""]
* Deletes content from the asset server.
* <p class="important">Not implemented: This method is not implemented yet.</p>
* @function Assets.deleteAsset
* @param {Assets.DeleteOptions} options - The content to delete and delete options.
* @param {object} scope - The scope that the <code>callback</code> function is defined in.
* @param {Assets~deleteAssetCallback} callback - The function to call upon completion.
*/
Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* Called when an {@link Assets.resolveAsset} call is complete.
* @callback Assets~resolveAssetCallback
* @param {string} error - <code>null</code> if the asset hash or path was resolved, otherwise a description of the error.
* @param {Assets.ResolveResult} result - Information on the hash or path resolved.
*/
/**jsdoc
* Resolves and returns information on a hash or a path in the asset server.
* @function Assets.resolveAsset
* @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying
* what to resolve. If a string, it may have a leading <code>"atp:"</code>.
* @param {object|Assets.CallbackDetails|Assets~resolveAssetCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~resolveAssetCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
* @example <caption>Get the hash and URL for a path.</caption>
* Assets.resolveAsset(
* "/assetsExamples/helloWorld.txt",
* function (error, result) {
* if (error) {
* print("ERROR: " + error);
* } else {
* print("Hash: " + result.hash);
* print("URL: " + result.url);
* }
* }
* );
*/
Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* @function Assets.decompressData
* @param {} options
* @param {} scope
* @param {} [callback = ""]
* Called when an {@link Assets.decompressData} call is complete.
* @callback Assets~decompressDataCallback
* @param {string} error - <code>null</code> if the data was successfully compressed, otherwise a description of the error.
* @param {Assets.DecompressResult} result - Information on and the decompressed data.
*/
/**jsdoc
* Decompresses data in memory using gunzip.
* @function Assets.decompressData
* @param {Assets.DecompressOptions} source - What to decompress and decompression options.
* @param {object|Assets.CallbackDetails|Assets~decompressDataCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~decompressDataCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
*/
Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* @function Assets.compressData
* @param {} options
* @param {} scope
* @param {} [callback = ""]
* Called when an {@link Assets.compressData} call is complete.
* @callback Assets~compressDataCallback
* @param {string} error - <code>null</code> if the data was successfully compressed, otherwise a description of the error.
* @param {Assets.CompressResult} result - Information on and the compressed data.
*/
/**jsdoc
* Compresses data in memory using gzip.
* @function Assets.compressData
* @param {string|ArrayBuffer|Assets.CompressOptions} source - What to compress and compression options. If a string or
* ArrayBuffer, the data to compress.
* @param {object|Assets.CallbackDetails|Assets~compressDataCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~compressDataCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
*/
Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* Initializes the cache if it isn't already initialized.
* @function Assets.initializeCache
* @returns {boolean}
* @returns {boolean} <code>true</code> if the cache is initialized, <code>false</code> if it isn't.
*/
Q_INVOKABLE bool initializeCache() { return Parent::initializeCache(); }
/**jsdoc
* Checks whether the script can write to the cache.
* @function Assets.canWriteCacheValue
* @param {string} url
* @returns {boolean}
* @param {string} url - <em>Not used.</em>
* @returns {boolean} <code>true</code> if the script is an Interface, avatar, or assignment client script,
* <code>false</code> if the script is a client entity or server entity script.
* @example <caption>Report whether the script can write to the cache.</caption>
* print("Can write to cache: " + Assets.canWriteCacheValue(null));
*/
Q_INVOKABLE bool canWriteCacheValue(const QUrl& url);
/**jsdoc
* @function Assets.getCacheStatus
* @param {} scope
* @param {} [callback=undefined]
* Called when a {@link Assets.getCacheStatus} call is complete.
* @callback Assets~getCacheStatusCallback
* @param {string} error - <code>null</code> if the cache status was retrieved without error, otherwise a description of
* the error.
* @param {Assets.GetCacheStatusResult} result - Details of the current cache status.
*/
/**jsdoc
* Gets the current cache status.
* @function Assets.getCacheStatus
* @param {object|Assets.CallbackDetails|Assets~getCacheStatusCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~getCacheStatusCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
* @example <caption>Report the cache status.</caption>
* Assets.getCacheStatus(function (error, status) {
* print("Cache status");
* print("- Error: " + error);
* print("- Status: " + JSON.stringify(status));
* });
*/
Q_INVOKABLE void getCacheStatus(QScriptValue scope, QScriptValue callback = QScriptValue()) {
jsPromiseReady(Parent::getCacheStatus(), scope, callback);
}
/**jsdoc
* @function Assets.queryCacheMeta
* @param {} options
* @param {} scope
* @param {} [callback=undefined]
* Called when {@link Assets.queryCacheMeta} is complete.
* @callback Assets~queryCacheMetaCallback
* @param {string} error - <code>null</code> if the URL has a valid cache entry, otherwise a description of the error.
* @param {Assets.CacheItemMetaData} result - Information on an asset in the cache.
*/
/**jsdoc
* Gets information about the status of an asset in the cache.
* @function Assets.queryCacheMeta
* @param {string|Assets.QueryCacheMetaOptions} path - The URL of the cached asset to get information on if a string,
* otherwise an object specifying the cached asset to get information on. The URL must start with <code>"atp:"</code>
* or <code>"cache:"</code>.
* @param {object|Assets.CallbackDetails|Assets~queryCacheMetaCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~queryCacheMetaCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
* @example <caption>Report details of a string store in the cache.</caption>
* Assets.queryCacheMeta(
* "cache:/cacheExample/helloCache.txt",
* function (error, result) {
* if (error) {
* print("Error: " + error);
* } else {
* print("Success:");
* print("- URL: " + result.url);
* print("- isValid: " + result.isValid);
* print("- saveToDisk: " + result.saveToDisk);
* print("- expirationDate: " + result.expirationDate);
* }
* }
* );
*/
Q_INVOKABLE void queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* @function Assets.loadFromCache
* @param {} options
* @param {} scope
* @param {} [callback=undefined]
* Called when an {@link Assets.loadFromCache} call is complete.
* @callback Assets~loadFromCacheCallback
* @param {string} error - <code>null</code> if the cache item was successfully retrieved, otherwise a description of the
* error.
* @param {Assets.LoadFromCacheResult} result - Information on and the retrieved data.
*/
/**jsdoc
* Retrieves data from the cache directly, without downloading it.
* @function Assets.loadFromCache
* @param {string|Assets.LoadFromCacheOptions} options - The URL of the asset to load from the cache if a string, otherwise
* an object specifying the asset to load from the cache and load options. The URL must start with <code>"atp:"</code>
* or <code>"cache:"</code>.
* @param {object|Assets.CallbackDetails|Assets~loadFromCacheCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~loadFromCacheCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
* @example <caption>Retrieve a string from the cache.</caption>
* Assets.loadFromCache(
* "cache:/cacheExample/helloCache.txt",
* function (error, result) {
* if (error) {
* print("Error: " + error);
* } else {
* print("Success:");
* print("- Response: " + result.response);
* print("- Content type: " + result.contentType);
* print("- Number of bytes: " + result.byteLength);
* print("- Bytes: " + [].slice.call(new Uint8Array(result.data), 0, result.byteLength));
* print("- URL: " + result.url);
* }
* }
* );
*/
Q_INVOKABLE void loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* @function Assets.saveToCache
* @param {} options
* @param {} scope
* @param {} [callback=undefined]
* Called when an {@link Assets.saveToCache} call is complete.
* @callback Assets~saveToCacheCallback
* @param {string} error - <code>null</code> if the asset data was successfully saved to the cache, otherwise a description
* of the error.
* @param {Assets.SaveToCacheResult} result - Information on the cached data.
*/
/**jsdoc
* Saves asset data to the cache directly, without downloading it from a URL.
* <p>Note: Can only be used in Interface, avatar, and assignment client scripts.</p>
* @function Assets.saveToCache
* @param {Assets.SaveToCacheOptions} options - The data to save to the cache and cache options.
* @param {object|Assets.CallbackDetails|Assets~saveToCacheCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~saveToCacheCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
* @example <caption>Save a string in the cache.</caption>
* Assets.saveToCache(
* {
* url: "cache:/cacheExample/helloCache.txt",
* data: "Hello cache"
* },
* function (error, result) {
* if (error) {
* print("Error: " + error);
* } else {
* print("Success:");
* print("- Bytes: " + result.byteLength);
* print("- URL: " + result.url);
* }
* }
* );
*/
Q_INVOKABLE void saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
/**jsdoc
* Saves asset data to the cache directly, without downloading it from a URL.
* <p>Note: Can only be used in Interface, avatar, and assignment client scripts.</p>
* @function Assets.saveToCache
* @param {} url
* @param {} data
* @param {} metadata
* @param {} scope
* @param {} [callback=undefined]
* @param {string} url - The URL to associate with the cache item. Must start with <code>"atp:"</code> or
* <code>"cache:"</code>.
* @param {string|ArrayBuffer} data - The data to save to the cache.
* @param {Assets.SaveToCacheHeaders} headers - The last-modified and expiry times for the cache item.
* @param {object|Assets.CallbackDetails|Assets~saveToCacheCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~saveToCacheCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>The function to call upon completion. May be an inline function, a function identifier, or the name of a function
* in a string. If the name of a function or a function identifier, it must be a member of the scope specified by
* <code>scopeOrCallback</code>.</p>
*/
Q_INVOKABLE void saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata,
QScriptValue scope, QScriptValue callback = QScriptValue());
protected:

View file

@ -237,6 +237,11 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const
}
}
QString ScriptEngine::getTypeAsString() const {
auto value = QVariant::fromValue(_type).toString();
return value.isEmpty() ? "unknown" : value.toLower();
}
QString ScriptEngine::getContext() const {
switch (_context) {
case CLIENT_SCRIPT:

View file

@ -122,6 +122,8 @@ public:
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
Q_OBJECT
Q_PROPERTY(QString context READ getContext)
Q_PROPERTY(QString type READ getTypeAsString)
Q_PROPERTY(QString fileName MEMBER _fileNameString CONSTANT)
public:
enum Context {
@ -138,6 +140,7 @@ public:
AGENT,
AVATAR
};
Q_ENUM(Type)
static int processLevelMaxRetries;
ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("about:ScriptEngine"));
@ -636,6 +639,7 @@ public:
void setType(Type type) { _type = type; };
Type getType() { return _type; };
QString getTypeAsString() const;
bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget
bool isRunning() const { return _isRunning; } // used by ScriptWidget

View file

@ -136,24 +136,6 @@ QUrl expandScriptUrl(const QUrl& rawScriptURL) {
QObject* scriptsModel();
bool NativeScriptInitializers::registerNativeScriptInitializer(NativeScriptInitializer initializer) {
return registerScriptInitializer([initializer](ScriptEnginePointer engine) {
initializer(qobject_cast<QScriptEngine*>(engine.data()));
});
}
bool NativeScriptInitializers::registerScriptInitializer(ScriptInitializer initializer) {
if (auto scriptEngines = DependencyManager::get<ScriptEngines>().data()) {
scriptEngines->registerScriptInitializer(initializer);
return true;
}
return false;
}
void ScriptEngines::registerScriptInitializer(ScriptInitializer initializer) {
_scriptInitializers.push_back(initializer);
}
void ScriptEngines::addScriptEngine(ScriptEnginePointer engine) {
if (!_isStopped) {
QMutexLocker locker(&_allScriptsMutex);
@ -590,12 +572,8 @@ void ScriptEngines::quitWhenFinished() {
}
int ScriptEngines::runScriptInitializers(ScriptEnginePointer scriptEngine) {
int ii=0;
for (auto initializer : _scriptInitializers) {
ii++;
initializer(scriptEngine);
}
return ii;
auto nativeCount = DependencyManager::get<ScriptInitializers>()->runScriptInitializers(scriptEngine.data());
return nativeCount + ScriptInitializerMixin<ScriptEnginePointer>::runScriptInitializers(scriptEngine);
}
void ScriptEngines::launchScriptEngine(ScriptEnginePointer scriptEngine) {

View file

@ -48,13 +48,8 @@ class ScriptEngine;
* scripts directory of the Interface installation.
* <em>Read-only.</em>
*/
class NativeScriptInitializers : public ScriptInitializerMixin {
public:
bool registerNativeScriptInitializer(NativeScriptInitializer initializer) override;
bool registerScriptInitializer(ScriptInitializer initializer) override;
};
class ScriptEngines : public QObject, public Dependency {
class ScriptEngines : public QObject, public Dependency, public ScriptInitializerMixin<ScriptEnginePointer> {
Q_OBJECT
Q_PROPERTY(ScriptsModel* scriptsModel READ scriptsModel CONSTANT)
@ -62,11 +57,9 @@ class ScriptEngines : public QObject, public Dependency {
Q_PROPERTY(QString debugScriptUrl READ getDebugScriptUrl WRITE setDebugScriptUrl)
public:
using ScriptInitializer = ScriptInitializerMixin::ScriptInitializer;
ScriptEngines(ScriptEngine::Context context, const QUrl& defaultScriptsOverride = QUrl());
void registerScriptInitializer(ScriptInitializer initializer);
int runScriptInitializers(ScriptEnginePointer engine);
int runScriptInitializers(ScriptEnginePointer engine) override;
void loadScripts();
void saveScripts();
@ -347,7 +340,6 @@ protected:
QHash<QUrl, ScriptEnginePointer> _scriptEnginesHash;
QSet<ScriptEnginePointer> _allKnownScriptEngines;
QMutex _allScriptsMutex;
std::list<ScriptInitializer> _scriptInitializers;
ScriptsModel _scriptsModel;
ScriptsModelFilter _scriptsModelFilter;
std::atomic<bool> _isStopped { false };

View file

@ -9,30 +9,38 @@
#pragma once
#include <functional>
#include <mutex>
#include <QSharedPointer>
#include "../DependencyManager.h"
class QScriptEngine;
class ScriptEngine;
class ScriptInitializerMixin : public QObject, public Dependency {
Q_OBJECT
template <typename T> class ScriptInitializerMixin {
public:
using ScriptInitializer = std::function<void(T)>;
virtual void registerScriptInitializer(ScriptInitializer initializer) {
InitializerLock lock(_scriptInitializerMutex);
_scriptInitializers.push_back(initializer);
}
virtual int runScriptInitializers(T engine) {
InitializerLock lock(_scriptInitializerMutex);
return std::count_if(_scriptInitializers.begin(), _scriptInitializers.end(),
[engine](auto initializer){ initializer(engine); return true; }
);
}
virtual ~ScriptInitializerMixin() {}
protected:
std::mutex _scriptInitializerMutex;
using InitializerLock = std::lock_guard<std::mutex>;
std::list<ScriptInitializer> _scriptInitializers;
};
class ScriptInitializers : public ScriptInitializerMixin<QScriptEngine*>, public Dependency {
public:
// Lightweight `QScriptEngine*` initializer (only depends on built-in Qt components)
// example registration:
// eg: [&](QScriptEngine* engine) -> bool {
// eg: [&](QScriptEngine* engine) {
// engine->globalObject().setProperties("API", engine->newQObject(...instance...))
// return true;
// }
using NativeScriptInitializer = std::function<void(QScriptEngine*)>;
virtual bool registerNativeScriptInitializer(NativeScriptInitializer initializer) = 0;
// Heavyweight `ScriptEngine*` initializer (tightly coupled to Interface and script-engine library internals)
// eg: [&](ScriptEnginePointer scriptEngine) -> bool {
// engine->registerGlobalObject("API", ...instance..);
// return true;
// }
using ScriptEnginePointer = QSharedPointer<ScriptEngine>;
using ScriptInitializer = std::function<void(ScriptEnginePointer)>;
virtual bool registerScriptInitializer(ScriptInitializer initializer) { return false; };
// };
};

View file

@ -90,6 +90,17 @@ public:
using Config = JobConfig;
};
/**jsdoc
* @namespace Workload
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*
* @property {number} cpuRunTime - <em>Read-only.</em>
* @property {boolean} enabled
* @property {number} branch
*/
// A default Config is always on; to create an enableable Config, use the ctor JobConfig(bool enabled)
class JobConfig : public QObject {
Q_OBJECT
@ -139,7 +150,7 @@ public:
double getCPURunTime() const { return _msCPURunTime; }
/**jsdoc
* @function Render.getConfig
* @function Workload.getConfig
* @param {string} name
* @returns {object}
*/
@ -162,19 +173,19 @@ public:
// Describe the node graph data connections of the associated Job/Task
/**jsdoc
* @function JobConfig.isTask
* @function Workload.isTask
* @returns {boolean}
*/
Q_INVOKABLE bool isTask() const { return _isTask; }
/**jsdoc
* @function JobConfig.isSwitch
* @function Workload.isSwitch
* @returns {boolean}
*/
Q_INVOKABLE bool isSwitch() const { return _isSwitch; }
/**jsdoc
* @function JobConfig.getSubConfigs
* @function Workload.getSubConfigs
* @returns {object[]}
*/
Q_INVOKABLE QObjectList getSubConfigs() const {
@ -187,13 +198,13 @@ public:
}
/**jsdoc
* @function JobConfig.getNumSubs
* @function Workload.getNumSubs
* @returns {number}
*/
Q_INVOKABLE int getNumSubs() const { return getSubConfigs().size(); }
/**jsdoc
* @function JobConfig.getSubConfig
* @function Workload.getSubConfig
* @param {number} index
* @returns {object}
*/
@ -214,7 +225,7 @@ public slots:
/**jsdoc
* @function Workload.load
* @param {object} map
* @param {object} json
*/
void load(const QJsonObject& val) { qObjectFromJsonValue(val, *this); emit loaded(); }

View file

@ -43,3 +43,7 @@ set(DIR "pcmCodec")
add_subdirectory(${DIR})
set(DIR "hifiCodec")
add_subdirectory(${DIR})
# example plugins
set(DIR "KasenAPIExample")
add_subdirectory(${DIR})

View file

@ -0,0 +1,3 @@
set(TARGET_NAME KasenAPIExample)
setup_hifi_client_server_plugin(scripting)
link_hifi_libraries(shared plugins avatars networking graphics gpu)

View file

@ -0,0 +1,56 @@
//
// ExampleScriptPlugin.h
// plugins/KasenAPIExample/src
//
// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com
// Copyright 2019 Kasen IO
//
// 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

View file

@ -0,0 +1,139 @@
//
// KasenAPIExample.cpp
// plugins/KasenAPIExample/src
//
// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com
// Copyright 2019 Kasen IO
//
// 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-6) {
out[keys.value(i)] = values[i];
}
}
return out;
}
struct _HeadHelper : public HeadData {
QMap<QString,int> getBlendshapeMap() const {
return _blendshapeLookupMap;
}
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"

View file

@ -0,0 +1,21 @@
{
"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
}
}
}

View file

@ -48,7 +48,6 @@ var UTF_CODE = 0;
// Only plays a sound if it is downloaded.
// Only plays one sound at a time.
var emojiCreateSound = SoundCache.getSound(Script.resolvePath('resources/sounds/emojiPopSound1.wav'));
var emojiDestroySound = SoundCache.getSound(Script.resolvePath('resources/sounds/emojiPopSound2.wav'));
var injector;
var DEFAULT_VOLUME = 0.01;
var local = false;
@ -326,9 +325,7 @@ function playPopAnimation() {
if (popType === "in") {
currentPopScale = MIN_POP_SCALE;
} else {
// Start with the pop sound on the out
currentPopScale = finalInPopScale ? finalInPopScale : MAX_POP_SCALE;
playSound(emojiDestroySound, DEFAULT_VOLUME, MyAvatar.position, true);
}
}

View file

@ -16,11 +16,13 @@
// *************************************
// #region dependencies
// The information needed to properly use the sprite sheets and get the general information
// about the emojis
var emojiList = Script.require("./emojiApp/resources/modules/emojiList.js");
var customEmojiList = Script.require("./emojiApp/resources/modules/customEmojiList.js");
// #endregion
// *************************************
// END dependencies
@ -181,6 +183,7 @@ function maybeClearClapSoundInterval() {
}
}
// URLs for this fn are relative to SimplifiedEmoteIndicator.qml
function toggleReaction(reaction) {
var reactionEnding = reactionsBegun.indexOf(reaction) > -1;
@ -192,6 +195,7 @@ function toggleReaction(reaction) {
}
}
function maybeDeleteRemoteIndicatorTimeout() {
if (restoreEmoteIndicatorTimeout) {
Script.clearTimeout(restoreEmoteIndicatorTimeout);
@ -199,6 +203,7 @@ function maybeDeleteRemoteIndicatorTimeout() {
}
}
var reactionsBegun = [];
var pointReticle = null;
var mouseMoveEventsConnected = false;
@ -229,6 +234,7 @@ function beginReactionWrapper(reaction) {
}
}
// Checks to see if there are any reticle entities already to delete
function deleteOldReticles() {
MyAvatar.getAvatarEntitiesVariant()
@ -313,6 +319,7 @@ function triggerReactionWrapper(reaction) {
}, WAIT_TO_RESTORE_EMOTE_INDICATOR_ICON_MS);
}
function maybeClearReticleUpdateLimiterTimeout() {
if (reticleUpdateRateLimiterTimer) {
Script.clearTimeout(reticleUpdateRateLimiterTimer);
@ -393,6 +400,7 @@ function onMessageFromEmoteAppBar(message) {
}
}
function getEmojiURLFromCode(code) {
var emojiObject = emojiList[emojiCodeMap[code]];
var emojiFilename;
@ -405,6 +413,7 @@ function getEmojiURLFromCode(code) {
return "../../emojiApp/resources/images/emojis/52px/" + emojiFilename;
}
function updateEmoteIndicatorIcon(iconURL) {
emoteAppBarWindow.sendToQml({
"source": "simplifiedEmote.js",
@ -451,7 +460,10 @@ function keyPressHandler(event) {
} else if (event.text === RAISE_HAND_KEY) {
toggleReaction("raiseHand");
} else if (event.text === APPLAUD_KEY) {
toggleReaction("applaud");
// Make sure this doesn't get triggered if you are flying, falling, or jumping
if (!MyAvatar.isInAir()) {
toggleReaction("applaud");
}
} else if (event.text === POINT_KEY) {
toggleReaction("point");
} else if (event.text === EMOTE_WINDOW && !(Settings.getValue("io.highfidelity.isEditing", false))) {
@ -639,6 +651,7 @@ function unload() {
// *************************************
// #region EMOJI_UTILITY
var EMOJI_52_BASE_URL = "../../resources/images/emojis/52px/";
function selectedEmoji(code) {
emojiAPI.addEmoji(code);
@ -744,6 +757,7 @@ function toggleEmojiApp() {
emojiAPI.registerAvimojiQMLWindow(emojiAppWindow);
}
// #endregion
// *************************************
// END EMOJI_MAIN

View file

@ -45,7 +45,8 @@ Rectangle {
}
Behavior on requestedWidth {
enabled: true
enabled: false // Set this to `true` once we have a different windowing system that better supports on-screen widgets
// like the Emote Indicator.
SmoothedAnimation { duration: 220 }
}

View file

@ -23,7 +23,7 @@
********************************************************************/
* {
box-sizing: border-box
box-sizing: border-box;
}
html
@ -38,11 +38,12 @@ body
font-weight: 400;
color: #000000;
letter-spacing: 0.5px;
font-size: 0.95rem;
line-height: 20px;
}
p {
font-size: 0.95rem;
line-height: 20px;
}
section
@ -128,7 +129,6 @@ table {
thead {
border-color: #d8e1d9;
background:#d8e1d9;
text-align: left;
}
table tr {
@ -146,6 +146,7 @@ td {
article table thead tr th, article table tbody tr td, article table tbody tr td p {
font-size: .89rem;
line-height: 20px;
text-align: left;
}
article table thead tr th, article table tbody tr td {
@ -199,6 +200,7 @@ nav {
padding-left: 20px;
padding-right: 20px;
box-sizing: border-box;
line-height: 12px;
}
nav::-webkit-scrollbar {
@ -378,12 +380,12 @@ nav > h2 > a {
tt, code, kbd, samp {
font-family: Consolas, Monaco, 'Andale Mono', monospace;
font-size: 0.9rem;
font-size: 1.05em;
}
.name, .signature {
font-family: Consolas, Monaco, 'Andale Mono', monospace;
font-size: 0.9rem;
font-size: 1.05em;
}
img {
@ -422,7 +424,6 @@ header {
display: block;
text-align: center;
font-size: 90%;
margin-top: -20px;
}
.variation {
@ -537,7 +538,7 @@ header {
.prettyprint code
{
font-size: 0.7rem;
font-size: 0.9em;
line-height: 18px;
display: block;
padding: 4px 12px;

View file

@ -92,35 +92,33 @@
</tr>
</tbody>
</table>
<?js= self.partial('details.tmpl', doc) ?>
<?js if (doc.examples && doc.examples.length) { ?>
<h3>Example<?js= doc.examples.length > 1? 's':'' ?></h3>
<?js= self.partial('examples.tmpl', doc.examples) ?>
<?js } ?>
<?js } ?>
<?js } else { ?>
<h3>Description</h3>
<?js if (doc.description) { ?>
<p><?js= doc.description ?></p>
<?js } ?>
<?js
var classes = self.find({kind: 'class', memberof: doc.longname});
if (!isGlobalPage && classes && classes.length) {
?>
<h3 id="#class">Classes</h3>
<?js classes.forEach(function(c) { ?>
<p><?js= self.linkto(c.longname, c.name) ?></p>
<?js }); ?>
<?js } ?>
<?js= self.partial('details.tmpl', doc) ?>
<?js if (doc.examples && doc.examples.length) { ?>
<h3>Example<?js= doc.examples.length > 1? 's':'' ?></h3>
<?js= self.partial('examples.tmpl', doc.examples) ?>
<?js } ?>
<?js
var classes = self.find({kind: 'class', memberof: doc.longname});
if (!isGlobalPage && classes && classes.length) {
?>
<h3 id="#class">Classes</h3>
<?js classes.forEach(function(c) { ?>
<p><?js= self.linkto(c.longname, c.name) ?></p>
<?js }); ?>
<?js } ?>
<?js= self.partial('details.tmpl', doc) ?>
<?js } ?>
</div>

View file

@ -125,7 +125,7 @@ exports.handlers = {
if (rows.length > 0) {
var availableIn = "<p class='availableIn'><b>Supported Script Types:</b> " + rows.join(" &bull; ") + "</p>";
e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn;
e.doclet.description = availableIn + (e.doclet.description ? e.doclet.description : "");
}
}