diff --git a/doomsday/libdeng2/include/de/vector.h b/doomsday/libdeng2/include/de/vector.h index 4a9ba7571e..437ec05eb0 100644 --- a/doomsday/libdeng2/include/de/vector.h +++ b/doomsday/libdeng2/include/de/vector.h @@ -501,6 +501,7 @@ typedef Vector2 Vector2i; ///< 2-component vector of integer values. typedef Vector2 Vector2ui; ///< 2-component vector of unsigned integer values. typedef Vector2 Vector2f; ///< 2-component vector of floating point values. typedef Vector2 Vector2d; ///< 2-component vector of high-precision floating point values. +typedef Vector3 Vector3ub; ///< 3-component vector of unsigned byte values. typedef Vector3 Vector3i; ///< 3-component vector of integer values. typedef Vector3 Vector3ui; ///< 3-component vector of unsigned integer values. typedef Vector3 Vector3f; ///< 3-component vector of floating point values. diff --git a/doomsday/libshell/include/de/shell/protocol.h b/doomsday/libshell/include/de/shell/protocol.h index 87a4a97504..85fb8691e3 100644 --- a/doomsday/libshell/include/de/shell/protocol.h +++ b/doomsday/libshell/include/de/shell/protocol.h @@ -77,6 +77,54 @@ class LIBSHELL_PUBLIC LogEntryPacket : public Packet Entries _entries; }; +/** + * Packet containing information about the players' current positions, colors, + * and names. + */ +class LIBSHELL_PUBLIC PlayerInfoPacket : public Packet +{ +public: + struct Player + { + int number; + Vector2i position; + String name; + Vector3ub color; + + Player(int num = 0, + Vector2i const &pos = Vector2i(), + String const &plrName = "", + Vector3ub const &plrColor = Vector3ub()) + : number(num), + position(pos), + name(plrName), + color(plrColor) + {} + }; + + typedef QMap Players; + +public: + PlayerInfoPacket(); + + void add(Player const &player); + + int count() const; + + Player const &player(int number) const; + + Players players() const; + + // Implements ISerializable. + void operator >> (Writer &to) const; + void operator << (Reader &from); + + static Packet *fromBlock(Block const &block); + +private: + DENG2_PRIVATE(d) +}; + /** * Packet containing an outline of a map's lines. * @@ -146,7 +194,7 @@ class LIBSHELL_PUBLIC Protocol : public de::Protocol GameState, ///< Current state of the game (mode, map). Leaderboard, ///< Frags leaderboard. MapOutline, ///< Sectors of the map for visual overview. - PlayerPositions ///< Current player positions. + PlayerInfo ///< Current player names, colors, positions. }; public: diff --git a/doomsday/libshell/src/protocol.cpp b/doomsday/libshell/src/protocol.cpp index cbd5fdcdec..30a2372cb1 100644 --- a/doomsday/libshell/src/protocol.cpp +++ b/doomsday/libshell/src/protocol.cpp @@ -106,6 +106,72 @@ Packet *LogEntryPacket::fromBlock(Block const &block) return constructFromBlock(block, LOG_ENTRY_PACKET_TYPE); } +// PlayerInfoPacket ---------------------------------------------------------- + +static char const *PLAYER_INFO_PACKET_TYPE = "PlrI"; + +DENG2_PIMPL_NOREF(PlayerInfoPacket) +{ + Players players; +}; + +PlayerInfoPacket::PlayerInfoPacket() + : Packet(PLAYER_INFO_PACKET_TYPE), d(new Instance) +{} + +void PlayerInfoPacket::add(Player const &player) +{ + d->players.insert(player.number, player); +} + +int PlayerInfoPacket::count() const +{ + return d->players.size(); +} + +PlayerInfoPacket::Player const &PlayerInfoPacket::player(int number) const +{ + DENG2_ASSERT(d->players.contains(number)); + return d->players[number]; +} + +PlayerInfoPacket::Players PlayerInfoPacket::players() const +{ + return d->players; +} + +void PlayerInfoPacket::operator >> (Writer &to) const +{ + Packet::operator >> (to); + + to << duint32(d->players.size()); + foreach(Player const &p, d->players) + { + to << dbyte(p.number) << p.position << p.name << p.color; + } +} + +void PlayerInfoPacket::operator << (Reader &from) +{ + d->players.clear(); + + Packet::operator << (from); + + duint32 count; + from >> count; + while(count-- > 0) + { + Player p; + from.readAs(p.number) >> p.position >> p.name >> p.color; + d->players.insert(p.number, p); + } +} + +Packet *PlayerInfoPacket::fromBlock(Block const &block) +{ + return constructFromBlock(block, PLAYER_INFO_PACKET_TYPE); +} + // MapOutlinePacket ---------------------------------------------------------- static char const *MAP_OUTLINE_PACKET_TYPE = "MpOL"; @@ -184,6 +250,7 @@ Protocol::Protocol() define(ChallengePacket::fromBlock); define(LogEntryPacket::fromBlock); define(MapOutlinePacket::fromBlock); + define(PlayerInfoPacket::fromBlock); } Protocol::PacketType Protocol::recognize(Packet const *packet) @@ -206,6 +273,12 @@ Protocol::PacketType Protocol::recognize(Packet const *packet) return MapOutline; } + if(packet->type() == PLAYER_INFO_PACKET_TYPE) + { + DENG2_ASSERT(dynamic_cast(packet) != 0); + return PlayerInfo; + } + // One of the generic-format packets? RecordPacket const *rec = dynamic_cast(packet); if(rec) diff --git a/doomsday/server/include/shelluser.h b/doomsday/server/include/shelluser.h index 43ded4f2ac..71bb01dc2a 100644 --- a/doomsday/server/include/shelluser.h +++ b/doomsday/server/include/shelluser.h @@ -54,6 +54,7 @@ class ShellUser : public de::shell::Link void sendGameState(); void sendMapOutline(); + void sendPlayerInfo(); protected slots: void handleIncomingPackets(); diff --git a/doomsday/server/include/shellusers.h b/doomsday/server/include/shellusers.h index 0d48f187c9..77fdf8e9e4 100644 --- a/doomsday/server/include/shellusers.h +++ b/doomsday/server/include/shellusers.h @@ -50,11 +50,14 @@ class ShellUsers : public QObject, public IMapChangeObserver void currentMapChanged(); +public slots: + void sendPlayerInfoToAll(); + protected slots: void userDisconnected(); private: - QSet _users; + DENG2_PRIVATE(d) }; #endif // SERVER_SHELLUSERS_H diff --git a/doomsday/server/src/shelluser.cpp b/doomsday/server/src/shelluser.cpp index 2b75f9bd5d..5889b4c1b7 100644 --- a/doomsday/server/src/shelluser.cpp +++ b/doomsday/server/src/shelluser.cpp @@ -29,7 +29,9 @@ #include "games.h" #include "Game" #include "def_main.h" +#include "network/net_main.h" #include "map/gamemap.h" +#include "map/p_players.h" using namespace de; @@ -96,6 +98,7 @@ void ShellUser::sendInitialUpdate() sendGameState(); sendMapOutline(); + sendPlayerInfo(); } void ShellUser::sendGameState() @@ -164,6 +167,37 @@ void ShellUser::sendMapOutline() *this << *packet; } +void ShellUser::sendPlayerInfo() +{ + if(!theMap) return; + + QScopedPointer packet(new shell::PlayerInfoPacket); + + for(uint i = 1; i < DDMAXPLAYERS; ++i) + { + if(!ddPlayers[i].shared.inGame || !ddPlayers[i].shared.mo) + continue; + + shell::PlayerInfoPacket::Player info; + + info.number = i; + info.name = clients[i].name; + info.position = de::Vector2i(ddPlayers[i].shared.mo->origin[VX], + ddPlayers[i].shared.mo->origin[VY]); + + /** + * @todo Player color is presently game-side data. Therefore, this + * packet should be constructed by libcommon (or player color should be + * moved to the engine). + */ + // info.color = ? + + packet->add(info); + } + + *this << *packet; +} + void ShellUser::handleIncomingPackets() { forever diff --git a/doomsday/server/src/shellusers.cpp b/doomsday/server/src/shellusers.cpp index cff12460da..b3b1c72c25 100644 --- a/doomsday/server/src/shellusers.cpp +++ b/doomsday/server/src/shellusers.cpp @@ -18,17 +18,43 @@ */ #include "shellusers.h" +#include -ShellUsers::ShellUsers() +static int const PLAYER_INFO_INTERVAL = 2500; // ms + +DENG2_PIMPL_NOREF(ShellUsers) +{ + QSet users; + QTimer *infoTimer; + + Instance() + { + infoTimer = new QTimer; + infoTimer->setInterval(PLAYER_INFO_INTERVAL); + } + + ~Instance() + { + delete infoTimer; + } +}; + +ShellUsers::ShellUsers() : d(new Instance) { audienceForMapChange += this; + + // Player information is sent periodically to all shell users. + connect(d->infoTimer, SIGNAL(timeout()), this, SLOT(sendPlayerInfoToAll())); + d->infoTimer->start(); } ShellUsers::~ShellUsers() { + d->infoTimer->stop(); + audienceForMapChange -= this; - foreach(ShellUser *user, _users) + foreach(ShellUser *user, d->users) { delete user; } @@ -38,7 +64,7 @@ void ShellUsers::add(ShellUser *user) { LOG_INFO("New shell user from %s") << user->address(); - _users.insert(user); + d->users.insert(user); connect(user, SIGNAL(disconnected()), this, SLOT(userDisconnected())); user->sendInitialUpdate(); @@ -46,15 +72,24 @@ void ShellUsers::add(ShellUser *user) int ShellUsers::count() const { - return _users.size(); + return d->users.size(); } void ShellUsers::currentMapChanged() { - foreach(ShellUser *user, _users) + foreach(ShellUser *user, d->users) { user->sendGameState(); user->sendMapOutline(); + user->sendPlayerInfo(); + } +} + +void ShellUsers::sendPlayerInfoToAll() +{ + foreach(ShellUser *user, d->users) + { + user->sendPlayerInfo(); } } @@ -63,7 +98,7 @@ void ShellUsers::userDisconnected() DENG2_ASSERT(dynamic_cast(sender()) != 0); ShellUser *user = static_cast(sender()); - _users.remove(user); + d->users.remove(user); LOG_INFO("Shell user from %s has disconnected") << user->address(); diff --git a/doomsday/tools/shell/shell-gui/src/linkwindow.cpp b/doomsday/tools/shell/shell-gui/src/linkwindow.cpp index 91afcb5f66..74b975617c 100644 --- a/doomsday/tools/shell/shell-gui/src/linkwindow.cpp +++ b/doomsday/tools/shell/shell-gui/src/linkwindow.cpp @@ -431,6 +431,10 @@ void LinkWindow::handleIncomingPackets() d->status->setMapOutline(*static_cast(packet.data())); break; + case shell::Protocol::PlayerInfo: + d->status->setPlayerInfo(*static_cast(packet.data())); + break; + default: break; } diff --git a/doomsday/tools/shell/shell-gui/src/statuswidget.cpp b/doomsday/tools/shell/shell-gui/src/statuswidget.cpp index d278c6625d..ea16ca49b4 100644 --- a/doomsday/tools/shell/shell-gui/src/statuswidget.cpp +++ b/doomsday/tools/shell/shell-gui/src/statuswidget.cpp @@ -30,17 +30,19 @@ DENG2_PIMPL(StatusWidget) { QFont smallFont; QFont largeFont; + QFont playerFont; String gameMode; String map; QPicture mapOutline; QRect mapBounds; shell::Link *link; + typedef shell::PlayerInfoPacket::Player Player; + shell::PlayerInfoPacket::Players players; + QMap oldPlayerPositions; + Instance(Public &i) : Base(i), link(0) - { - //gameMode = "Ultimate DOOM"; - //map = "E1M3"; - } + {} void clear() { @@ -54,10 +56,11 @@ DENG2_PIMPL(StatusWidget) StatusWidget::StatusWidget(QWidget *parent) : QWidget(parent), d(new Instance(*this)) { - d->smallFont = d->largeFont = font(); + d->playerFont = d->smallFont = d->largeFont = font(); d->smallFont.setPointSize(font().pointSize() * 3 / 4); d->largeFont.setPointSize(font().pointSize() * 3 / 2); d->largeFont.setBold(true); + d->playerFont.setPointSizeF(font().pointSizeF() * .75f); } void StatusWidget::setGameState(QString mode, QString rules, QString mapId, QString mapTitle) @@ -101,6 +104,17 @@ void StatusWidget::setMapOutline(shell::MapOutlinePacket const &outline) update(); } +void StatusWidget::setPlayerInfo(shell::PlayerInfoPacket const &plrInfo) +{ + foreach(Instance::Player const &plr, d->players) + { + d->oldPlayerPositions[plr.number] = QPoint(plr.position.x, -plr.position.y); + } + + d->players = plrInfo.players(); + update(); +} + void StatusWidget::paintEvent(QPaintEvent *) { if(!d->link) @@ -137,7 +151,7 @@ void StatusWidget::paintEvent(QPaintEvent *) viewSize.setHeight(outlineRect.width() / mapRatio); if(viewSize.height() > outlineRect.height()) { - // Doesn't fit this way, fit to vertically instead. + // Doesn't fit this way, fit vertically instead. viewSize.setHeight(outlineRect.height()); viewSize.setWidth(outlineRect.height() * mapRatio); } @@ -147,6 +161,67 @@ void StatusWidget::paintEvent(QPaintEvent *) painter.setRenderHint(QPainter::Antialiasing, true); painter.drawPicture(0, 0, d->mapOutline); + + // Draw player markers. + float const factor = float(d->mapBounds.width()) / float(viewSize.width()); + QFontMetrics const metrics(d->playerFont); + foreach(Instance::Player const &plr, d->players.values()) + { + QColor color(plr.color.x, plr.color.y, plr.color.z); + + QColor markColor = color; + markColor.setAlpha(180); + + QPoint plrPos(plr.position.x, -plr.position.y); + + if(d->oldPlayerPositions.contains(plr.number)) + { + QPointF const start = d->oldPlayerPositions[plr.number]; + QPointF const end = plrPos; + QPointF const delta = end - start; + + /// @todo Qt has no gradient support for drawing lines? + + int const STOPS = 100; + for(int i = 0; i < STOPS; ++i) + { + QColor grad = color; + grad.setAlpha(i * 100 / STOPS); + float a = float(i) / float(STOPS); + float b = float(i + 1) / float(STOPS); + QPen gradPen(grad); + gradPen.setWidthF(2 * factor); + painter.setPen(gradPen); + painter.drawLine(start + a * delta, start + b * delta); + } + } + + painter.setTransform(QTransform::fromScale(factor, factor) * + QTransform::fromTranslate(plrPos.x(), plrPos.y())); + + painter.setPen(color); + painter.setBrush(markColor); + painter.drawEllipse(QPoint(0, 0), 4, 4); + painter.drawLine(QPoint(0, 4), QPoint(0, 10)); + markColor.setAlpha(100); + painter.setBrush(markColor); + + QString label = QString("%1: %2").arg(plr.number).arg(plr.name); + if(label.size() > 20) label = label.left(20); + + QRect textBounds = metrics.boundingRect(label); + int const gap = 3; + textBounds.moveTopLeft(QPoint(-textBounds.width()/2, 10 + gap)); + QRect boxBounds = textBounds.adjusted(-gap, -gap, gap, metrics.descent() + gap); + painter.drawRoundedRect(boxBounds, 2, 2); + + painter.setFont(d->playerFont); + + painter.setPen(Qt::black); + painter.drawText(textBounds.topLeft() + QPoint(0, metrics.ascent()), label); + painter.setPen(Qt::white); + painter.drawText(textBounds.topLeft() + QPoint(0, metrics.ascent() - 1), label); + } } } diff --git a/doomsday/tools/shell/shell-gui/src/statuswidget.h b/doomsday/tools/shell/shell-gui/src/statuswidget.h index 9b370beb26..b820a97ffa 100644 --- a/doomsday/tools/shell/shell-gui/src/statuswidget.h +++ b/doomsday/tools/shell/shell-gui/src/statuswidget.h @@ -36,6 +36,7 @@ class StatusWidget : public QWidget void setGameState(QString mode, QString rules, QString mapId, QString mapTitle); void setMapOutline(de::shell::MapOutlinePacket const &outline); + void setPlayerInfo(de::shell::PlayerInfoPacket const &plrInfo); void paintEvent(QPaintEvent *);