mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-04-10 10:34:56 +02:00
Merge branch 'master' of https://github.com/worklist/hifi into multi_VS_assigments
This commit is contained in:
commit
ad4dea129c
21 changed files with 217 additions and 71 deletions
9
VoxelScriptingInterface.cpp
Normal file
9
VoxelScriptingInterface.cpp
Normal file
|
@ -0,0 +1,9 @@
|
|||
//
|
||||
// VoxelScriptingInterface.cpp
|
||||
// hifi
|
||||
//
|
||||
// Created by Stephen Birarda on 9/17/13.
|
||||
// Copyright (c) 2013 HighFidelity, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#include "VoxelScriptingInterface.h"
|
|
@ -24,4 +24,12 @@ include(${MACRO_DIR}/LinkHifiLibrary.cmake)
|
|||
link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR})
|
||||
link_hifi_library(audio ${TARGET_NAME} ${ROOT_DIR})
|
||||
link_hifi_library(avatars ${TARGET_NAME} ${ROOT_DIR})
|
||||
link_hifi_library(voxel-server-library ${TARGET_NAME} ${ROOT_DIR})
|
||||
link_hifi_library(voxels ${TARGET_NAME} ${ROOT_DIR})
|
||||
link_hifi_library(voxel-server-library ${TARGET_NAME} ${ROOT_DIR})
|
||||
|
||||
# link the stk library
|
||||
set(STK_ROOT_DIR ${ROOT_DIR}/externals/stk)
|
||||
find_package(STK REQUIRED)
|
||||
target_link_libraries(${TARGET_NAME} ${STK_LIBRARIES})
|
||||
include_directories(${STK_INCLUDE_DIRS})
|
||||
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
#include <QtScript/QScriptEngine>
|
||||
#include <QtNetwork/QtNetwork>
|
||||
|
||||
#include <AvatarData.h>
|
||||
#include <NodeList.h>
|
||||
|
||||
#include "AvatarData.h"
|
||||
|
||||
#include "Agent.h"
|
||||
#include "voxels/VoxelScriptingInterface.h"
|
||||
|
||||
Agent::Agent() :
|
||||
_shouldStop(false)
|
||||
|
@ -23,7 +23,7 @@ Agent::Agent() :
|
|||
|
||||
void Agent::run(QUrl scriptURL) {
|
||||
NodeList::getInstance()->setOwnerType(NODE_TYPE_AGENT);
|
||||
NodeList::getInstance()->setNodeTypesOfInterest(&NODE_TYPE_AVATAR_MIXER, 1);
|
||||
NodeList::getInstance()->setNodeTypesOfInterest(&NODE_TYPE_VOXEL_SERVER, 1);
|
||||
|
||||
QNetworkAccessManager manager;
|
||||
|
||||
|
@ -39,14 +39,13 @@ void Agent::run(QUrl scriptURL) {
|
|||
|
||||
QScriptEngine engine;
|
||||
|
||||
AvatarData *testAvatarData = new AvatarData;
|
||||
|
||||
QScriptValue avatarDataValue = engine.newQObject(testAvatarData);
|
||||
engine.globalObject().setProperty("Avatar", avatarDataValue);
|
||||
|
||||
QScriptValue agentValue = engine.newQObject(this);
|
||||
engine.globalObject().setProperty("Agent", agentValue);
|
||||
|
||||
VoxelScriptingInterface voxelScripter;
|
||||
QScriptValue voxelScripterValue = engine.newQObject(&voxelScripter);
|
||||
engine.globalObject().setProperty("Voxels", voxelScripterValue);
|
||||
|
||||
qDebug() << "Downloaded script:" << scriptString << "\n";
|
||||
qDebug() << "Evaluated script:" << engine.evaluate(scriptString).toString() << "\n";
|
||||
|
||||
|
@ -75,9 +74,10 @@ void Agent::run(QUrl scriptURL) {
|
|||
NodeList::getInstance()->sendDomainServerCheckIn();
|
||||
}
|
||||
|
||||
// allow the scripter's call back to setup visual data
|
||||
emit preSendCallback();
|
||||
|
||||
testAvatarData->sendData();
|
||||
// flush the voxel packet queue
|
||||
voxelScripter.getVoxelPacketSender()->process();
|
||||
|
||||
if (NodeList::getInstance()->getNodeSocket()->receive((sockaddr*) &senderAddress, receivedData, &receivedBytes)) {
|
||||
NodeList::getInstance()->processNodeData((sockaddr*) &senderAddress, receivedData, receivedBytes);
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
#include "Agent.h"
|
||||
#include <Assignment.h>
|
||||
#include <AudioMixer.h>
|
||||
#include <AvatarMixer.h>
|
||||
#include "audio/AudioMixer.h"
|
||||
#include "avatars/AvatarMixer.h"
|
||||
#include <Logging.h>
|
||||
#include <NodeList.h>
|
||||
#include <PacketHeaders.h>
|
||||
|
@ -31,6 +31,7 @@ const char CHILD_TARGET_NAME[] = "assignment-client";
|
|||
pid_t* childForks = NULL;
|
||||
sockaddr_in customAssignmentSocket = {};
|
||||
int numForks = 0;
|
||||
Assignment::Type overiddenAssignmentType = Assignment::AllTypes;
|
||||
|
||||
void childClient() {
|
||||
// this is one of the child forks or there is a single assignment client, continue assignment-client execution
|
||||
|
@ -56,8 +57,8 @@ void childClient() {
|
|||
|
||||
sockaddr_in senderSocket = {};
|
||||
|
||||
// create a request assignment, accept all assignments, pass the desired pool (if it exists)
|
||||
Assignment requestAssignment(Assignment::RequestCommand, Assignment::AllTypes);
|
||||
// create a request assignment, accept assignments defined by the overidden type
|
||||
Assignment requestAssignment(Assignment::RequestCommand, ::overiddenAssignmentType);
|
||||
|
||||
while (true) {
|
||||
if (usecTimestampNow() - usecTimestamp(&lastRequest) >= ASSIGNMENT_REQUEST_INTERVAL_USECS) {
|
||||
|
@ -211,6 +212,15 @@ int main(int argc, const char* argv[]) {
|
|||
::customAssignmentSocket = socketForHostnameAndHostOrderPort(customAssignmentServerHostname, assignmentServerPort);
|
||||
}
|
||||
|
||||
const char ASSIGNMENT_TYPE_OVVERIDE_OPTION[] = "-t";
|
||||
const char* assignmentTypeString = getCmdOption(argc, argv, ASSIGNMENT_TYPE_OVVERIDE_OPTION);
|
||||
|
||||
if (assignmentTypeString) {
|
||||
// the user is asking to only be assigned to a particular type of assignment
|
||||
// so set that as the ::overridenAssignmentType to be used in requests
|
||||
::overiddenAssignmentType = (Assignment::Type) atoi(assignmentTypeString);
|
||||
}
|
||||
|
||||
const char* NUM_FORKS_PARAMETER = "-n";
|
||||
const char* numForksString = getCmdOption(argc, argv, NUM_FORKS_PARAMETER);
|
||||
|
||||
|
|
15
assignment-client/src/voxels/VoxelScriptingInterface.cpp
Normal file
15
assignment-client/src/voxels/VoxelScriptingInterface.cpp
Normal file
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// VoxelScriptingInterface.cpp
|
||||
// hifi
|
||||
//
|
||||
// Created by Stephen Birarda on 9/17/13.
|
||||
// Copyright (c) 2013 HighFidelity, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#include "VoxelScriptingInterface.h"
|
||||
|
||||
void VoxelScriptingInterface::queueVoxelAdd(float x, float y, float z, float scale, uchar red, uchar green, uchar blue) {
|
||||
// setup a VoxelDetail struct with the data
|
||||
VoxelDetail addVoxelDetail = {x, y, z, scale, red, green, blue};
|
||||
_voxelPacketSender.sendVoxelEditMessage(PACKET_TYPE_SET_VOXEL, addVoxelDetail);
|
||||
}
|
36
assignment-client/src/voxels/VoxelScriptingInterface.h
Normal file
36
assignment-client/src/voxels/VoxelScriptingInterface.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// VoxelScriptingInterface.h
|
||||
// hifi
|
||||
//
|
||||
// Created by Stephen Birarda on 9/17/13.
|
||||
// Copyright (c) 2013 HighFidelity, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef __hifi__VoxelScriptingInterface__
|
||||
#define __hifi__VoxelScriptingInterface__
|
||||
|
||||
#include <QtCore/QObject>
|
||||
|
||||
#include <VoxelEditPacketSender.h>
|
||||
|
||||
/// handles scripting of voxel commands from JS passed to assigned clients
|
||||
class VoxelScriptingInterface : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
VoxelEditPacketSender* getVoxelPacketSender() { return &_voxelPacketSender; }
|
||||
public slots:
|
||||
/// queues the creation of a voxel which will be sent by calling process on the PacketSender
|
||||
/// \param x the x-coordinate of the voxel (in VS space)
|
||||
/// \param y the y-coordinate of the voxel (in VS space)
|
||||
/// \param z the z-coordinate of the voxel (in VS space)
|
||||
/// \param scale the scale of the voxel (in VS space)
|
||||
/// \param red the R value for RGB color of voxel
|
||||
/// \param green the G value for RGB color of voxel
|
||||
/// \param blue the B value for RGB color of voxel
|
||||
void queueVoxelAdd(float x, float y, float z, float scale, uchar red, uchar green, uchar blue);
|
||||
private:
|
||||
/// attached VoxelEditPacketSender that handles queuing and sending of packets to VS
|
||||
VoxelEditPacketSender _voxelPacketSender;
|
||||
};
|
||||
|
||||
#endif /* defined(__hifi__VoxelScriptingInterface__) */
|
|
@ -62,30 +62,39 @@ int main(int argc, const char* argv[]) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// check if the requestor is on the same network as the destination for the assignment
|
||||
if (senderSocket.sin_addr.s_addr ==
|
||||
((sockaddr_in*) (*assignment)->getAttachedPublicSocket())->sin_addr.s_addr) {
|
||||
// if this is the case we remove the public socket on the assignment by setting it to NULL
|
||||
// this ensures the local IP and port sent to the requestor is the local address of destination
|
||||
(*assignment)->setAttachedPublicSocket(NULL);
|
||||
if (requestAssignment.getType() == Assignment::AllTypes ||
|
||||
(*assignment)->getType() == requestAssignment.getType()) {
|
||||
// give this assignment out, either we have a type match or the requestor has said they will
|
||||
// take all types
|
||||
|
||||
// check if the requestor is on the same network as the destination for the assignment
|
||||
if (senderSocket.sin_addr.s_addr ==
|
||||
((sockaddr_in*) (*assignment)->getAttachedPublicSocket())->sin_addr.s_addr) {
|
||||
// if this is the case we remove the public socket on the assignment by setting it to NULL
|
||||
// this ensures the local IP and port sent to the requestor is the local address of destination
|
||||
(*assignment)->setAttachedPublicSocket(NULL);
|
||||
}
|
||||
|
||||
|
||||
int numAssignmentBytes = (*assignment)->packToBuffer(assignmentPacket + numSendHeaderBytes);
|
||||
|
||||
// send the assignment
|
||||
serverSocket.send((sockaddr*) &senderSocket,
|
||||
assignmentPacket,
|
||||
numSendHeaderBytes + numAssignmentBytes);
|
||||
|
||||
|
||||
// delete this assignment now that it has been sent out
|
||||
delete *assignment;
|
||||
// remove it from the deque and make the iterator the next assignment
|
||||
assignmentQueue.erase(assignment);
|
||||
|
||||
// stop looping - we've handed out an assignment
|
||||
break;
|
||||
} else {
|
||||
// push forward the iterator to check the next assignment
|
||||
assignment++;
|
||||
}
|
||||
|
||||
|
||||
int numAssignmentBytes = (*assignment)->packToBuffer(assignmentPacket + numSendHeaderBytes);
|
||||
|
||||
// send the assignment
|
||||
serverSocket.send((sockaddr*) &senderSocket,
|
||||
assignmentPacket,
|
||||
numSendHeaderBytes + numAssignmentBytes);
|
||||
|
||||
|
||||
// delete this assignment now that it has been sent out
|
||||
delete *assignment;
|
||||
// remove it from the deque and make the iterator the next assignment
|
||||
assignmentQueue.erase(assignment);
|
||||
|
||||
// stop looping - we've handed out an assignment
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (senderData[0] == PACKET_TYPE_CREATE_ASSIGNMENT && packetVersionMatch(senderData)) {
|
||||
|
|
|
@ -2,7 +2,15 @@ MACRO(SETUP_HIFI_PROJECT TARGET INCLUDE_QT)
|
|||
project(${TARGET})
|
||||
|
||||
# grab the implemenation and header files
|
||||
file(GLOB TARGET_SRCS src/*.cpp src/*.h src/*.c)
|
||||
file(GLOB TARGET_SRCS src/*)
|
||||
|
||||
file(GLOB SRC_SUBDIRS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/*)
|
||||
foreach(DIR ${SRC_SUBDIRS})
|
||||
if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/${DIR})
|
||||
FILE(GLOB DIR_CONTENTS src/${DIR}/*)
|
||||
SET(TARGET_SRCS ${TARGET_SRCS} ${DIR_CONTENTS})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# add the executable
|
||||
add_executable(${TARGET} ${TARGET_SRCS})
|
||||
|
|
|
@ -36,11 +36,22 @@ body {
|
|||
}
|
||||
#deploy-button {
|
||||
background-color: #0DFFBB;
|
||||
right: 0px;
|
||||
right: 85px;
|
||||
}
|
||||
#deploy-button:hover {
|
||||
background-color: #28FF57;
|
||||
}
|
||||
|
||||
#instance-field {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
#instance-field input {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
#stop-button {
|
||||
background-color: #CC1F00;
|
||||
right: 0px;
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
Run
|
||||
</a>
|
||||
</div>
|
||||
<div class='big-field' id='instance-field'>
|
||||
<input type='text' name='instances' placeholder='# of instances'>
|
||||
</div>
|
||||
<!-- %div#stop-button.big-button -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -20,6 +20,11 @@ $(document).ready(function(){
|
|||
// add the script
|
||||
+ script + '\r\n'
|
||||
+ '--' + boundary + '--';
|
||||
|
||||
var headers = {};
|
||||
if ($('#instance-field input').val()) {
|
||||
headers['ASSIGNMENT-INSTANCES'] = $('#instance-field input').val();
|
||||
}
|
||||
|
||||
// post form to assignment in order to create an assignment
|
||||
$.ajax({
|
||||
|
@ -27,6 +32,7 @@ $(document).ready(function(){
|
|||
data: body,
|
||||
type: "POST",
|
||||
url: "/assignment",
|
||||
headers: headers,
|
||||
success: function (data, status) {
|
||||
console.log(data);
|
||||
console.log(status);
|
||||
|
|
|
@ -111,9 +111,20 @@ static int mongooseRequestHandler(struct mg_connection *conn) {
|
|||
const char ASSIGNMENT_SCRIPT_HOST_LOCATION[] = "resources/web/assignment";
|
||||
|
||||
static void mongooseUploadHandler(struct mg_connection *conn, const char *path) {
|
||||
|
||||
// create an assignment for this saved script, for now make it local only
|
||||
Assignment *scriptAssignment = new Assignment(Assignment::CreateCommand, Assignment::AgentType, Assignment::LocalLocation);
|
||||
|
||||
// check how many instances of this assignment the user wants by checking the ASSIGNMENT-INSTANCES header
|
||||
const char ASSIGNMENT_INSTANCES_HTTP_HEADER[] = "ASSIGNMENT-INSTANCES";
|
||||
const char *requestInstancesHeader = mg_get_header(conn, ASSIGNMENT_INSTANCES_HTTP_HEADER);
|
||||
|
||||
if (requestInstancesHeader) {
|
||||
// the user has requested a number of instances greater than 1
|
||||
// so set that on the created assignment
|
||||
scriptAssignment->setNumberOfInstances(atoi(requestInstancesHeader));
|
||||
}
|
||||
|
||||
QString newPath(ASSIGNMENT_SCRIPT_HOST_LOCATION);
|
||||
newPath += "/";
|
||||
// append the UUID for this script as the new filename, remove the curly braces
|
||||
|
@ -129,7 +140,6 @@ static void mongooseUploadHandler(struct mg_connection *conn, const char *path)
|
|||
::assignmentQueueMutex.lock();
|
||||
::assignmentQueue.push_back(scriptAssignment);
|
||||
::assignmentQueueMutex.unlock();
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, const char* argv[]) {
|
||||
|
@ -355,25 +365,39 @@ int main(int argc, const char* argv[]) {
|
|||
std::deque<Assignment*>::iterator assignment = ::assignmentQueue.begin();
|
||||
|
||||
while (assignment != ::assignmentQueue.end()) {
|
||||
// construct the requested assignment from the packet data
|
||||
Assignment requestAssignment(packetData, receivedBytes);
|
||||
|
||||
// give this assignment out, no conditions stop us from giving it to the local assignment client
|
||||
int numHeaderBytes = populateTypeAndVersion(broadcastPacket, PACKET_TYPE_CREATE_ASSIGNMENT);
|
||||
int numAssignmentBytes = (*assignment)->packToBuffer(broadcastPacket + numHeaderBytes);
|
||||
|
||||
nodeList->getNodeSocket()->send((sockaddr*) &nodePublicAddress,
|
||||
broadcastPacket,
|
||||
numHeaderBytes + numAssignmentBytes);
|
||||
|
||||
// remove the assignment from the queue
|
||||
::assignmentQueue.erase(assignment);
|
||||
|
||||
if ((*assignment)->getType() == Assignment::AgentType) {
|
||||
// if this is a script assignment we need to delete it to avoid a memory leak
|
||||
delete *assignment;
|
||||
if (requestAssignment.getType() == Assignment::AllTypes ||
|
||||
(*assignment)->getType() == requestAssignment.getType()) {
|
||||
// give this assignment out, either the type matches or the requestor said they will take any
|
||||
int numHeaderBytes = populateTypeAndVersion(broadcastPacket, PACKET_TYPE_CREATE_ASSIGNMENT);
|
||||
int numAssignmentBytes = (*assignment)->packToBuffer(broadcastPacket + numHeaderBytes);
|
||||
|
||||
nodeList->getNodeSocket()->send((sockaddr*) &nodePublicAddress,
|
||||
broadcastPacket,
|
||||
numHeaderBytes + numAssignmentBytes);
|
||||
|
||||
if ((*assignment)->getType() == Assignment::AgentType) {
|
||||
// if this is a script assignment we need to delete it to avoid a memory leak
|
||||
// or if there is more than one instance to send out, simpy decrease the number of instances
|
||||
if ((*assignment)->getNumberOfInstances() > 1) {
|
||||
(*assignment)->decrementNumberOfInstances();
|
||||
} else {
|
||||
::assignmentQueue.erase(assignment);
|
||||
delete *assignment;
|
||||
}
|
||||
} else {
|
||||
// remove the assignment from the queue
|
||||
::assignmentQueue.erase(assignment);
|
||||
}
|
||||
|
||||
// stop looping, we've handed out an assignment
|
||||
break;
|
||||
} else {
|
||||
// push forward the iterator to check the next assignment
|
||||
assignment++;
|
||||
}
|
||||
|
||||
// stop looping, we've handed out an assignment
|
||||
break;
|
||||
}
|
||||
|
||||
::assignmentQueueMutex.unlock();
|
||||
|
@ -397,12 +421,18 @@ int main(int argc, const char* argv[]) {
|
|||
|
||||
nodeList->sendAssignment(*(*assignment));
|
||||
|
||||
// remove the assignment from the queue
|
||||
::assignmentQueue.erase(assignment);
|
||||
|
||||
if ((*assignment)->getType() == Assignment::AgentType) {
|
||||
// if this is a script assignment we need to delete it to avoid a memory leak
|
||||
delete *assignment;
|
||||
// or if there is more than one instance to send out, simpy decrease the number of instances
|
||||
if ((*assignment)->getNumberOfInstances() > 1) {
|
||||
(*assignment)->decrementNumberOfInstances();
|
||||
} else {
|
||||
::assignmentQueue.erase(assignment);
|
||||
delete *assignment;
|
||||
}
|
||||
} else {
|
||||
// remove the assignment from the queue
|
||||
::assignmentQueue.erase(assignment);
|
||||
}
|
||||
|
||||
// stop looping, we've handed out an assignment
|
||||
|
|
|
@ -16,10 +16,4 @@ include(${MACRO_DIR}/IncludeGLM.cmake)
|
|||
include_glm(${TARGET_NAME} ${ROOT_DIR})
|
||||
|
||||
include(${MACRO_DIR}/LinkHifiLibrary.cmake)
|
||||
link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR})
|
||||
|
||||
# link the stk library
|
||||
set(STK_ROOT_DIR ${ROOT_DIR}/externals/stk)
|
||||
find_package(STK REQUIRED)
|
||||
target_link_libraries(${TARGET_NAME} ${STK_LIBRARIES})
|
||||
include_directories(${STK_INCLUDE_DIRS})
|
||||
link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR})
|
|
@ -21,7 +21,8 @@ Assignment::Assignment(Assignment::Command command, Assignment::Type type, Assig
|
|||
_type(type),
|
||||
_location(location),
|
||||
_attachedPublicSocket(NULL),
|
||||
_attachedLocalSocket(NULL)
|
||||
_attachedLocalSocket(NULL),
|
||||
_numberOfInstances(1)
|
||||
{
|
||||
// set the create time on this assignment
|
||||
gettimeofday(&_time, NULL);
|
||||
|
@ -35,7 +36,8 @@ Assignment::Assignment(Assignment::Command command, Assignment::Type type, Assig
|
|||
Assignment::Assignment(const unsigned char* dataBuffer, int numBytes) :
|
||||
_location(GlobalLocation),
|
||||
_attachedPublicSocket(NULL),
|
||||
_attachedLocalSocket(NULL)
|
||||
_attachedLocalSocket(NULL),
|
||||
_numberOfInstances(1)
|
||||
{
|
||||
// set the create time on this assignment
|
||||
gettimeofday(&_time, NULL);
|
||||
|
|
|
@ -56,6 +56,10 @@ public:
|
|||
Assignment::Location getLocation() const { return _location; }
|
||||
const timeval& getTime() const { return _time; }
|
||||
|
||||
int getNumberOfInstances() const { return _numberOfInstances; }
|
||||
void setNumberOfInstances(int numberOfInstances) { _numberOfInstances = numberOfInstances; }
|
||||
void decrementNumberOfInstances() { --_numberOfInstances; }
|
||||
|
||||
const sockaddr* getAttachedPublicSocket() { return _attachedPublicSocket; }
|
||||
void setAttachedPublicSocket(const sockaddr* attachedPublicSocket);
|
||||
|
||||
|
@ -78,6 +82,7 @@ private:
|
|||
sockaddr* _attachedPublicSocket; /// pointer to a public socket that relates to assignment, depends on direction
|
||||
sockaddr* _attachedLocalSocket; /// pointer to a local socket that relates to assignment, depends on direction
|
||||
timeval _time; /// time the assignment was created (set in constructor)
|
||||
int _numberOfInstances; /// the number of instances of this assignment
|
||||
};
|
||||
|
||||
QDebug operator<<(QDebug debug, const Assignment &assignment);
|
||||
|
|
Loading…
Reference in a new issue