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

This commit is contained in:
samcake 2016-02-25 14:14:04 -08:00
commit 610189ded3
13 changed files with 391 additions and 80 deletions

View file

@ -835,41 +835,40 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) {
if (audioEnvGroupObject[AUDIO_ZONES].isObject()) {
const QJsonObject& zones = audioEnvGroupObject[AUDIO_ZONES].toObject();
const QString X_RANGE = "x_range";
const QString Y_RANGE = "y_range";
const QString Z_RANGE = "z_range";
const QString X_MIN = "x_min";
const QString X_MAX = "x_max";
const QString Y_MIN = "y_min";
const QString Y_MAX = "y_max";
const QString Z_MIN = "z_min";
const QString Z_MAX = "z_max";
foreach (const QString& zone, zones.keys()) {
QJsonObject zoneObject = zones[zone].toObject();
if (zoneObject.contains(X_RANGE) && zoneObject.contains(Y_RANGE) && zoneObject.contains(Z_RANGE)) {
QStringList xRange = zoneObject.value(X_RANGE).toString().split("-", QString::SkipEmptyParts);
QStringList yRange = zoneObject.value(Y_RANGE).toString().split("-", QString::SkipEmptyParts);
QStringList zRange = zoneObject.value(Z_RANGE).toString().split("-", QString::SkipEmptyParts);
if (zoneObject.contains(X_MIN) && zoneObject.contains(X_MAX) && zoneObject.contains(Y_MIN) &&
zoneObject.contains(Y_MAX) && zoneObject.contains(Z_MIN) && zoneObject.contains(Z_MAX)) {
if (xRange.size() == 2 && yRange.size() == 2 && zRange.size() == 2) {
float xMin, xMax, yMin, yMax, zMin, zMax;
bool ok, allOk = true;
xMin = xRange[0].toFloat(&ok);
allOk &= ok;
xMax = xRange[1].toFloat(&ok);
allOk &= ok;
yMin = yRange[0].toFloat(&ok);
allOk &= ok;
yMax = yRange[1].toFloat(&ok);
allOk &= ok;
zMin = zRange[0].toFloat(&ok);
allOk &= ok;
zMax = zRange[1].toFloat(&ok);
allOk &= ok;
float xMin, xMax, yMin, yMax, zMin, zMax;
bool ok, allOk = true;
xMin = zoneObject.value(X_MIN).toString().toFloat(&ok);
allOk &= ok;
xMax = zoneObject.value(X_MAX).toString().toFloat(&ok);
allOk &= ok;
yMin = zoneObject.value(Y_MIN).toString().toFloat(&ok);
allOk &= ok;
yMax = zoneObject.value(Y_MAX).toString().toFloat(&ok);
allOk &= ok;
zMin = zoneObject.value(Z_MIN).toString().toFloat(&ok);
allOk &= ok;
zMax = zoneObject.value(Z_MAX).toString().toFloat(&ok);
allOk &= ok;
if (allOk) {
glm::vec3 corner(xMin, yMin, zMin);
glm::vec3 dimensions(xMax - xMin, yMax - yMin, zMax - zMin);
AABox zoneAABox(corner, dimensions);
_audioZones.insert(zone, zoneAABox);
qDebug() << "Added zone:" << zone << "(corner:" << corner
<< ", dimensions:" << dimensions << ")";
}
if (allOk) {
glm::vec3 corner(xMin, yMin, zMin);
glm::vec3 dimensions(xMax - xMin, yMax - yMin, zMax - zMin);
AABox zoneAABox(corner, dimensions);
_audioZones.insert(zone, zoneAABox);
qDebug() << "Added zone:" << zone << "(corner:" << corner
<< ", dimensions:" << dimensions << ")";
}
}
}

View file

@ -230,22 +230,40 @@
},
"columns": [
{
"name": "x_range",
"label": "X range",
"name": "x_min",
"label": "X start",
"can_set": true,
"placeholder": "0-16384"
"placeholder": "-16384.0"
},
{
"name": "y_range",
"label": "Y range",
"name": "x_max",
"label": "X end",
"can_set": true,
"placeholder": "0-16384"
"placeholder": "16384.0"
},
{
"name": "y_min",
"label": "Y start",
"can_set": true,
"placeholder": "-16384.0"
},
{
"name": "z_range",
"label": "Z range",
"name": "y_max",
"label": "Y end",
"can_set": true,
"placeholder": "0-16384"
"placeholder": "16384.0"
},
{
"name": "z_min",
"label": "Z start",
"can_set": true,
"placeholder": "-16384.0"
},
{
"name": "z_max",
"label": "Z end",
"can_set": true,
"placeholder": "16384.0"
}
]
},

View file

@ -199,7 +199,7 @@ pointInExtents = function(point, minPoint, maxPoint) {
* @param Number l The lightness
* @return Array The RGB representation
*/
hslToRgb = function(hsl, hueOffset) {
hslToRgb = function(hsl) {
var r, g, b;
if (hsl.s == 0) {
r = g = b = hsl.l; // achromatic

View file

@ -43,10 +43,10 @@ var keysToAllow = [
'emitSpeed',
'speedSpread',
'emitOrientation',
'emitDimensios',
'emitRadiusStart',
'emitDimensions',
'polarStart',
'polarFinish',
'azimuthStart',
'azimuthFinish',
'emitAcceleration',
'accelerationSpread',

View file

@ -0,0 +1,229 @@
//
// fireworksLaunchEntityScript.js
// examples/playa/fireworks
//
// Created by Eric Levin on 2/24/16.
// Copyright 2016 High Fidelity, Inc.
//
// Run this script to spawn a big fireworks launch button that a user can press
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(function() {
Script.include("../../libraries/utils.js");
var _this;
Fireworks = function() {
_this = this;
_this.launchSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/missle+launch.wav");
_this.explosionSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/fireworksExplosion.wav");
_this.timeToExplosionRange = {
min: 2500,
max: 4500
};
};
Fireworks.prototype = {
startNearTrigger: function() {
_this.shootFireworks();
},
startFarTrigger: function() {
_this.shootFireworks();
},
clickReleaseOnEntity: function() {
_this.shootFireworks();
},
shootFireworks: function() {
// Get launch position
var launchPosition = getEntityUserData(_this.entityID).launchPosition || _this.position;
var numMissles = randInt(1, 3);
for(var i = 0; i < numMissles; i++) {
_this.shootMissle(launchPosition);
}
},
shootMissle: function(launchPosition) {
Audio.playSound(_this.launchSound, {
position: launchPosition,
volume: 0.5
});
var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Rocket-2.fbx";
var missleDimensions = Vec3.multiply({
x: 0.24,
y: 0.7,
z: 0.24
}, randFloat(0.2, 1.5));
var missleRotation = Quat.fromPitchYawRollDegrees(randInt(-60, 60), 0, randInt(-60, 60));
var missleVelocity = Vec3.multiply(Quat.getUp(missleRotation), randFloat(2, 4));
var missleAcceleration = Vec3.multiply(Quat.getUp(missleRotation), randFloat(1, 3));
var missle = Entities.addEntity({
type: "Model",
modelURL: MODEL_URL,
position: launchPosition,
rotation: missleRotation,
dimensions: missleDimensions,
damping: 0,
dynamic: true,
lifetime: 20, // Just in case
velocity: missleVelocity,
acceleration: missleAcceleration,
angularVelocity: {
x: 0,
y: randInt(-1, 1),
z: 0
},
angularDamping: 0,
visible: false
});
var smokeTrailPosition = Vec3.sum(launchPosition, Vec3.multiply(-missleDimensions.y / 2 + 0.1, Quat.getUp(missleRotation)));
var smoke = Entities.addEntity({
type: "ParticleEffect",
position: smokeTrailPosition,
lifespan: 10,
lifetime: 20,
name: "Smoke Trail",
maxParticles: 3000,
emitRate: 80,
emitSpeed: 0,
speedSpread: 0,
dimensions: {
x: 1000,
y: 1000,
z: 1000
},
polarStart: 0,
polarFinish: 0,
azimuthStart: -3.14,
azimuthFinish: 3.14,
emitAcceleration: {
x: 0,
y: 0.01,
z: 0
},
accelerationSpread: {
x: 0.01,
y: 0,
z: 0.01
},
radiusSpread: 0.03,
particleRadius: 0.3,
radiusStart: 0.06,
radiusFinish: 0.9,
alpha: 0.1,
alphaSpread: 0,
alphaStart: 0.7,
alphaFinish: 0,
textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png",
emitterShouldTrail: true,
parentID: missle,
});
Script.setTimeout(function() {
Entities.editEntity(smoke, {
parentID: null,
isEmitting: false
});
var explodeBasePosition = Entities.getEntityProperties(missle, "position").position;
Entities.deleteEntity(missle);
// Explode 1 firework immediately
_this.explodeFirework(explodeBasePosition);
var numAdditionalFireworks = randInt(1, 5);
for (var i = 0; i < numAdditionalFireworks; i++) {
Script.setTimeout(function() {
var explodePosition = Vec3.sum(explodeBasePosition, {x: randFloat(-3, 3), y: randFloat(-3, 3), z: randFloat(-3, 3)});
_this.explodeFirework(explodePosition);
}, randInt(0, 1000))
}
}, randFloat(_this.timeToExplosionRange.min, _this.timeToExplosionRange.max));
},
explodeFirework: function(explodePosition) {
// We just exploded firework, so stop emitting its fire and smoke
Audio.playSound(_this.explosionSound, {
position: explodePosition
});
var firework = Entities.addEntity({
name: "fireworks emitter",
position: explodePosition,
type: "ParticleEffect",
colorStart: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.7
}),
color: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.5
}),
colorFinish: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.7
}),
maxParticles: 10000,
lifetime: 20,
lifespan: randFloat(1.5, 3),
emitRate: randInt(500, 5000),
emitSpeed: randFloat(0.5, 2),
speedSpread: 0.2,
emitOrientation: Quat.fromPitchYawRollDegrees(randInt(0, 360), randInt(0, 360), randInt(0, 360)),
polarStart: 1,
polarFinish: randFloat(1.2, 3),
azimuthStart: -Math.PI,
azimuthFinish: Math.PI,
emitAcceleration: {
x: 0,
y: randFloat(-1, -0.2),
z: 0
},
accelerationSpread: {
x: Math.random(),
y: 0,
z: Math.random()
},
particleRadius: randFloat(0.001, 0.1),
radiusSpread: Math.random() * 0.1,
radiusStart: randFloat(0.001, 0.1),
radiusFinish: randFloat(0.001, 0.1),
alpha: randFloat(0.8, 1.0),
alphaSpread: randFloat(0.1, 0.2),
alphaStart: randFloat(0.7, 1.0),
alphaFinish: randFloat(0.7, 1.0),
textures: "http://ericrius1.github.io/PlatosCave/assets/star.png",
});
Script.setTimeout(function() {
Entities.editEntity(firework, {
isEmitting: false
});
}, randInt(500, 1000));
},
preload: function(entityID) {
_this.entityID = entityID;
_this.position = Entities.getEntityProperties(_this.entityID, "position").position;
print("EBL RELOAD ENTITY SCRIPT!!!");
}
};
// entity scripts always need to return a newly constructed object of our type
return new Fireworks();
});

View file

@ -0,0 +1,46 @@
//
// fireworksLaunchButtonSpawner.js
// examples/playa/fireworks
//
// Created by Eric Levina on 2/24/16.
// Copyright 2015 High Fidelity, Inc.
//
// Run this script to spawn a big fireworks launch button that a user can press
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
var orientation = Camera.getOrientation();
orientation = Quat.safeEulerAngles(orientation);
orientation.x = 0;
orientation = Quat.fromVec3Degrees(orientation);
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
// Math.random ensures no caching of script
var SCRIPT_URL = Script.resolvePath("fireworksLaunchButtonEntityScript.js");
var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Launch-Button.fbx";
var launchButton = Entities.addEntity({
type: "Model",
name: "launch pad",
modelURL: MODEL_URL,
position: center,
dimensions: {
x: 0.98,
y: 1.16,
z: 0.98
},
script: SCRIPT_URL,
userData: JSON.stringify({
launchPosition: {x: 1, y: 1.8, z: -20.9},
grabbableKey: {
wantsTrigger: true
}
})
})
function cleanup() {
Entities.deleteEntity(launchButton);
}
Script.scriptEnding.connect(cleanup);

View file

@ -70,17 +70,9 @@ Menu::Menu() {
dialogsManager.data(), &DialogsManager::toggleLoginDialog);
}
// File > Update -- FIXME: needs implementation
auto action = addActionToQMenuAndActionHash(fileMenu, "Update");
action->setDisabled(true);
// File > Help
addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp()));
// File > Crash Reporter...-- FIXME: needs implementation
auto crashReporterAction = addActionToQMenuAndActionHash(fileMenu, "Crash Reporter...");
crashReporterAction->setDisabled(true);
// File > About
addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole);
@ -167,7 +159,7 @@ Menu::Menu() {
QObject* avatar = avatarManager->getMyAvatar();
// Avatar > Attachments...
action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments);
auto action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments);
connect(action, &QAction::triggered, [] {
DependencyManager::get<OffscreenUi>()->show(QString("hifi/dialogs/AttachmentsDialog.qml"), "AttachmentsDialog");
});
@ -259,16 +251,10 @@ Menu::Menu() {
// Navigate menu ----------------------------------
MenuWrapper* navigateMenu = addMenu("Navigate");
// Navigate > Home -- FIXME: needs implementation
auto homeAction = addActionToQMenuAndActionHash(navigateMenu, "Home");
homeAction->setDisabled(true);
// Navigate > Show Address Bar
addActionToQMenuAndActionHash(navigateMenu, MenuOption::AddressBar, Qt::CTRL | Qt::Key_L,
dialogsManager.data(), SLOT(toggleAddressBar()));
// Navigate > Directory -- FIXME: needs implementation
addActionToQMenuAndActionHash(navigateMenu, "Directory");
// Navigate > Bookmark related menus -- Note: the Bookmark class adds its own submenus here.
qApp->getBookmarks()->setupMenus(this, navigateMenu);
@ -299,20 +285,19 @@ Menu::Menu() {
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), "GeneralPreferencesDialog");
});
// Settings > Avatar...-- FIXME: needs implementation
// Settings > Avatar...
action = addActionToQMenuAndActionHash(settingsMenu, "Avatar...");
connect(action, &QAction::triggered, [] {
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), "AvatarPreferencesDialog");
});
// Settings > Audio...-- FIXME: needs implementation
// Settings > Audio...
action = addActionToQMenuAndActionHash(settingsMenu, "Audio...");
connect(action, &QAction::triggered, [] {
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/AudioPreferencesDialog.qml"), "AudioPreferencesDialog");
});
// Settings > LOD...-- FIXME: needs implementation
// Settings > LOD...
action = addActionToQMenuAndActionHash(settingsMenu, "LOD...");
connect(action, &QAction::triggered, [] {
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/LodPreferencesDialog.qml"), "LodPreferencesDialog");

View file

@ -134,14 +134,14 @@ void EntityTreeRenderer::update() {
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
tree->update();
// check to see if the avatar has moved and if we need to handle enter/leave entity logic
checkEnterLeaveEntities();
// Handle enter/leave entity logic
bool updated = checkEnterLeaveEntities();
// even if we haven't changed positions, if we previously attempted to set the skybox, but
// have a pending download of the skybox texture, then we should attempt to reapply to
// get the correct texture.
if ((_pendingSkyboxTexture && _skyboxTexture && _skyboxTexture->isLoaded()) ||
(_pendingAmbientTexture && _ambientTexture && _ambientTexture->isLoaded())) {
// If we haven't already updated and previously attempted to load a texture,
// check if the texture loaded and apply it
if (!updated && (
(_pendingSkyboxTexture && _skyboxTexture && _skyboxTexture->isLoaded()) ||
(_pendingAmbientTexture && _ambientTexture && _ambientTexture->isLoaded()))) {
applyZonePropertiesToScene(_bestZone);
}
@ -157,7 +157,7 @@ void EntityTreeRenderer::update() {
deleteReleasedModels();
}
void EntityTreeRenderer::checkEnterLeaveEntities() {
bool EntityTreeRenderer::checkEnterLeaveEntities() {
if (_tree && !_shuttingDown) {
glm::vec3 avatarPosition = _viewState->getAvatarPosition();
@ -172,7 +172,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
std::static_pointer_cast<EntityTree>(_tree)->findEntities(avatarPosition, radius, foundEntities);
// Whenever you're in an intersection between zones, we will always choose the smallest zone.
_bestZone = NULL; // NOTE: Is this what we want?
_bestZone = nullptr; // NOTE: Is this what we want?
_bestZoneVolume = std::numeric_limits<float>::max();
// create a list of entities that actually contain the avatar's position
@ -205,7 +205,6 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
}
applyZonePropertiesToScene(_bestZone);
});
// Note: at this point we don't need to worry about the tree being locked, because we only deal with
@ -229,8 +228,11 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
}
_currentEntitiesInside = entitiesContainingAvatar;
_lastAvatarPosition = avatarPosition;
return true;
}
}
return false;
}
void EntityTreeRenderer::leaveAllEntities() {
@ -851,3 +853,19 @@ void EntityTreeRenderer::updateEntityRenderStatus(bool shouldRenderEntities) {
}
}
}
void EntityTreeRenderer::updateZone(const EntityItemID& id) {
if (!_bestZone) {
// Get in the zone!
auto zone = getTree()->findEntityByEntityItemID(id);
if (zone && zone->contains(_lastAvatarPosition)) {
_currentEntitiesInside << id;
emit enterEntity(id);
_entitiesScriptEngine->callEntityScriptMethod(id, "enterEntity");
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(zone);
}
}
if (_bestZone && _bestZone->getID() == id) {
applyZonePropertiesToScene(_bestZone);
}
}

View file

@ -109,6 +109,7 @@ public slots:
void entitySciptChanging(const EntityItemID& entityID, const bool reload);
void entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision);
void updateEntityRenderStatus(bool shouldRenderEntities);
void updateZone(const EntityItemID& id);
// optional slots that can be wired to menu items
void setDisplayModelBounds(bool value) { _displayModelBounds = value; }
@ -136,11 +137,11 @@ private:
EntityItemID _currentClickingOnEntityID;
QScriptValueList createEntityArgs(const EntityItemID& entityID);
void checkEnterLeaveEntities();
bool checkEnterLeaveEntities();
void leaveAllEntities();
void forceRecheckEntities();
glm::vec3 _lastAvatarPosition;
glm::vec3 _lastAvatarPosition { 0.0f };
QVector<EntityItemID> _currentEntitiesInside;
bool _pendingSkyboxTexture { false };

View file

@ -18,6 +18,7 @@
#include <GeometryCache.h>
#include <PerfStat.h>
#include "EntityTreeRenderer.h"
#include "RenderableEntityItem.h"
// Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1
@ -62,6 +63,10 @@ bool RenderableZoneEntityItem::setProperties(const EntityItemProperties& propert
return somethingChanged;
}
void RenderableZoneEntityItem::somethingChangedNotification() {
DependencyManager::get<EntityTreeRenderer>()->updateZone(_id);
}
int RenderableZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead,
ReadBitstreamToTreeParams& args,
EntityPropertyFlags& propertyFlags, bool overwriteLocalData,

View file

@ -28,6 +28,8 @@ public:
{ }
virtual bool setProperties(const EntityItemProperties& properties);
virtual void somethingChangedNotification() override;
virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead,
ReadBitstreamToTreeParams& args,
EntityPropertyFlags& propertyFlags, bool overwriteLocalData,

View file

@ -26,9 +26,7 @@ LogHandler& LogHandler::getInstance() {
return staticInstance;
}
LogHandler::LogHandler() :
_shouldOutputProcessID(false),
_shouldOutputThreadID(false)
LogHandler::LogHandler()
{
// setup our timer to flush the verbose logs every 5 seconds
QTimer* logFlushTimer = new QTimer(this);
@ -62,6 +60,9 @@ const char* stringForLogType(LogMsgType msgType) {
// the following will produce 11/18 13:55:36
const QString DATE_STRING_FORMAT = "MM/dd hh:mm:ss";
// the following will produce 11/18 13:55:36.999
const QString DATE_STRING_FORMAT_WITH_MILLISECONDS = "MM/dd hh:mm:ss.zzz";
void LogHandler::flushRepeatedMessages() {
QMutexLocker locker(&_repeatedMessageLock);
QHash<QString, int>::iterator message = _repeatMessageCountHash.begin();
@ -132,7 +133,12 @@ QString LogHandler::printMessage(LogMsgType type, const QMessageLogContext& cont
// log prefix is in the following format
// [TIMESTAMP] [DEBUG] [PID] [TID] [TARGET] logged string
QString prefixString = QString("[%1]").arg(QDateTime::currentDateTime().toString(DATE_STRING_FORMAT));
const QString* dateFormatPtr = &DATE_STRING_FORMAT;
if (_shouldDisplayMilliseconds) {
dateFormatPtr = &DATE_STRING_FORMAT_WITH_MILLISECONDS;
}
QString prefixString = QString("[%1]").arg(QDateTime::currentDateTime().toString(*dateFormatPtr));
prefixString.append(QString(" [%1]").arg(stringForLogType(type)));

View file

@ -42,6 +42,7 @@ public:
void setShouldOutputProcessID(bool shouldOutputProcessID) { _shouldOutputProcessID = shouldOutputProcessID; }
void setShouldOutputThreadID(bool shouldOutputThreadID) { _shouldOutputThreadID = shouldOutputThreadID; }
void setShouldDisplayMilliseconds(bool shouldDisplayMilliseconds) { _shouldDisplayMilliseconds = shouldDisplayMilliseconds; }
QString printMessage(LogMsgType type, const QMessageLogContext& context, const QString &message);
@ -57,8 +58,9 @@ private:
void flushRepeatedMessages();
QString _targetName;
bool _shouldOutputProcessID;
bool _shouldOutputThreadID;
bool _shouldOutputProcessID { false };
bool _shouldOutputThreadID { false };
bool _shouldDisplayMilliseconds { false };
QSet<QString> _repeatedMessageRegexes;
QHash<QString, int> _repeatMessageCountHash;
QHash<QString, QString> _lastRepeatedMessage;