openspades/Sources/Client/NetClient.cpp
Rakete175 7cf405cf40 NetClient.cpp: Actually send Enhanced version packet
Currently OpenSpades doesnt send the extended version info which contains
some useful info to the server even when being told that it wants it.

This commit adds sending of the packet so servers will receive such info when
they ask for it.
2021-11-14 23:11:06 +01:00

1943 lines
57 KiB
C++

/*
Copyright (c) 2013 yvt
based on code of pysnip (c) Mathias Kaerlev 2011-2012.
This file is part of OpenSpades.
OpenSpades is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenSpades is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OpenSpades. If not, see <http://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <string.h>
#include <vector>
#include <enet/enet.h>
#include "CTFGameMode.h"
#include "Client.h"
#include "GameMap.h"
#include "GameMapLoader.h"
#include "GameProperties.h"
#include "Grenade.h"
#include "NetClient.h"
#include "Player.h"
#include "TCGameMode.h"
#include "World.h"
#include <Core/CP437.h>
#include <Core/Debug.h>
#include <Core/DeflateStream.h>
#include <Core/Exception.h>
#include <Core/Math.h>
#include <Core/MemoryStream.h>
#include <Core/Settings.h>
#include <Core/Strings.h>
#include <Core/TMPUtils.h>
DEFINE_SPADES_SETTING(cg_unicode, "1");
namespace spades {
namespace client {
namespace {
const char UtfSign = -1;
enum { BLUE_FLAG = 0, GREEN_FLAG = 1, BLUE_BASE = 2, GREEN_BASE = 3 };
enum PacketType {
PacketTypePositionData = 0,
PacketTypeOrientationData = 1,
PacketTypeWorldUpdate = 2,
PacketTypeInputData = 3,
PacketTypeWeaponInput = 4,
PacketTypeHitPacket = 5, // C2S
PacketTypeSetHP = 5, // S2C
PacketTypeGrenadePacket = 6,
PacketTypeSetTool = 7,
PacketTypeSetColour = 8,
PacketTypeExistingPlayer = 9,
PacketTypeShortPlayerData = 10,
PacketTypeMoveObject = 11,
PacketTypeCreatePlayer = 12,
PacketTypeBlockAction = 13,
PacketTypeBlockLine = 14,
PacketTypeStateData = 15,
PacketTypeKillAction = 16,
PacketTypeChatMessage = 17,
PacketTypeMapStart = 18, // S2C
PacketTypeMapChunk = 19, // S2C
PacketTypePlayerLeft = 20, // S2P
PacketTypeTerritoryCapture = 21, // S2P
PacketTypeProgressBar = 22,
PacketTypeIntelCapture = 23, // S2P
PacketTypeIntelPickup = 24, // S2P
PacketTypeIntelDrop = 25, // S2P
PacketTypeRestock = 26, // S2P
PacketTypeFogColour = 27, // S2C
PacketTypeWeaponReload = 28, // C2S2P
PacketTypeChangeTeam = 29, // C2S2P
PacketTypeChangeWeapon = 30, // C2S2P
PacketTypeMapCached = 31, // S2C
PacketTypeHandShakeInit = 31, // S2C
PacketTypeHandShakeReturn = 32, // C2S
PacketTypeVersionGet = 33, // S2C
PacketTypeVersionSend = 34, // C2S
PacketTypeExtensionInfo = 60,
};
enum class VersionInfoPropertyId : std::uint8_t {
ApplicationNameAndVersion = 0,
UserLocale = 1,
ClientFeatureFlags1 = 2
};
enum class ClientFeatureFlags1 : std::uint32_t { None = 0, SupportsUnicode = 1 << 0 };
ClientFeatureFlags1 operator|(ClientFeatureFlags1 a, ClientFeatureFlags1 b) {
return (ClientFeatureFlags1)((uint32_t)a | (uint32_t)b);
}
ClientFeatureFlags1 &operator|=(ClientFeatureFlags1 &a, ClientFeatureFlags1 b) {
return a = a | b;
}
std::string EncodeString(std::string str) {
auto str2 = CP437::Encode(str, -1);
if (!cg_unicode) {
// ignore fallbacks
return str2;
}
if (str2.find(-1) != std::string::npos) {
// some fallbacks; always use UTF8
str.insert(0, &UtfSign, 1);
} else {
str = str2;
}
return str;
}
std::string DecodeString(std::string s) {
if (s.size() > 0 && s[0] == UtfSign) {
return s.substr(1);
}
return CP437::Decode(s);
}
} // namespace
class NetPacketReader {
std::vector<char> data;
size_t pos;
public:
NetPacketReader(ENetPacket *packet) {
SPADES_MARK_FUNCTION();
data.resize(packet->dataLength);
memcpy(data.data(), packet->data, packet->dataLength);
enet_packet_destroy(packet);
pos = 1;
}
NetPacketReader(const std::vector<char> inData) {
data = inData;
pos = 1;
}
PacketType GetType() { return (PacketType)data[0]; }
uint32_t ReadInt() {
SPADES_MARK_FUNCTION();
uint32_t value = 0;
if (pos + 4 > data.size()) {
SPRaise("Received packet truncated");
}
value |= ((uint32_t)(uint8_t)data[pos++]);
value |= ((uint32_t)(uint8_t)data[pos++]) << 8;
value |= ((uint32_t)(uint8_t)data[pos++]) << 16;
value |= ((uint32_t)(uint8_t)data[pos++]) << 24;
return value;
}
uint16_t ReadShort() {
SPADES_MARK_FUNCTION();
uint32_t value = 0;
if (pos + 2 > data.size()) {
SPRaise("Received packet truncated");
}
value |= ((uint32_t)(uint8_t)data[pos++]);
value |= ((uint32_t)(uint8_t)data[pos++]) << 8;
return (uint16_t)value;
}
uint8_t ReadByte() {
SPADES_MARK_FUNCTION();
if (pos >= data.size()) {
SPRaise("Received packet truncated");
}
return (uint8_t)data[pos++];
}
float ReadFloat() {
SPADES_MARK_FUNCTION();
union {
float f;
uint32_t v;
};
v = ReadInt();
return f;
}
IntVector3 ReadIntColor() {
SPADES_MARK_FUNCTION();
IntVector3 col;
col.z = ReadByte();
col.y = ReadByte();
col.x = ReadByte();
return col;
}
Vector3 ReadFloatColor() {
SPADES_MARK_FUNCTION();
Vector3 col;
col.z = ReadByte() / 255.f;
col.y = ReadByte() / 255.f;
col.x = ReadByte() / 255.f;
return col;
}
std::size_t GetNumRemainingBytes() { return data.size() - pos; }
std::vector<char> GetData() { return data; }
std::string ReadData(size_t siz) {
if (pos + siz > data.size()) {
SPRaise("Received packet truncated");
}
std::string s = std::string(data.data() + pos, siz);
pos += siz;
return s;
}
std::string ReadRemainingData() {
return std::string(data.data() + pos, data.size() - pos);
}
std::string ReadString(size_t siz) {
// convert to C string once so that
// null-chars are removed
std::string s = ReadData(siz).c_str();
s = DecodeString(s);
return s;
}
std::string ReadRemainingString() {
// convert to C string once so that
// null-chars are removed
std::string s = ReadRemainingData().c_str();
s = DecodeString(s);
return s;
}
void DumpDebug() {
#if 1
char buf[1024];
std::string str;
sprintf(buf, "Packet 0x%02x [len=%d]", (int)GetType(), (int)data.size());
str = buf;
int bytes = (int)data.size();
if (bytes > 64) {
bytes = 64;
}
for (int i = 0; i < bytes; i++) {
sprintf(buf, " %02x", (unsigned int)(unsigned char)data[i]);
str += buf;
}
SPLog("%s", str.c_str());
#endif
}
};
class NetPacketWriter {
std::vector<char> data;
public:
NetPacketWriter(PacketType type) { data.push_back(type); }
void Write(uint8_t v) {
SPADES_MARK_FUNCTION_DEBUG();
data.push_back(v);
}
void Write(uint16_t v) {
SPADES_MARK_FUNCTION_DEBUG();
data.push_back((char)(v));
data.push_back((char)(v >> 8));
}
void Write(uint32_t v) {
SPADES_MARK_FUNCTION_DEBUG();
data.push_back((char)(v));
data.push_back((char)(v >> 8));
data.push_back((char)(v >> 16));
data.push_back((char)(v >> 24));
}
void Write(float v) {
SPADES_MARK_FUNCTION_DEBUG();
union {
float f;
uint32_t i;
};
f = v;
Write(i);
}
void WriteColor(IntVector3 v) {
Write((uint8_t)v.z);
Write((uint8_t)v.y);
Write((uint8_t)v.x);
}
void Write(std::string str) {
str = EncodeString(str);
data.insert(data.end(), str.begin(), str.end());
}
void Write(std::string str, size_t fillLen) {
str = EncodeString(str);
Write(str.substr(0, fillLen));
size_t sz = str.size();
while (sz < fillLen) {
Write((uint8_t)0);
sz++;
}
}
std::size_t GetPosition() { return data.size(); }
void Update(std::size_t position, std::uint8_t newValue) {
SPADES_MARK_FUNCTION_DEBUG();
if (position >= data.size()) {
SPRaise("Invalid write (%d should be less than %d)", (int)position,
(int)data.size());
}
data[position] = static_cast<char>(newValue);
}
void Update(std::size_t position, std::uint32_t newValue) {
SPADES_MARK_FUNCTION_DEBUG();
if (position + 4 > data.size()) {
SPRaise("Invalid write (%d should be less than or equal to %d)",
(int)(position + 4), (int)data.size());
}
// Assuming the target platform is little endian and supports
// unaligned memory access...
*reinterpret_cast<std::uint32_t *>(data.data() + position) = newValue;
}
ENetPacket *CreatePacket(int flag = ENET_PACKET_FLAG_RELIABLE) {
return enet_packet_create(data.data(), data.size(), flag);
}
};
NetClient::NetClient(Client *c) : client(c), host(nullptr), peer(nullptr) {
SPADES_MARK_FUNCTION();
enet_initialize();
SPLog("ENet initialized");
host = enet_host_create(NULL, 1, 1, 100000, 100000);
SPLog("ENet host created");
if (!host) {
SPRaise("Failed to create ENet host");
}
if (enet_host_compress_with_range_coder(host) < 0)
SPRaise("Failed to enable ENet Range coder.");
SPLog("ENet Range Coder Enabled");
peer = NULL;
status = NetClientStatusNotConnected;
lastPlayerInput = 0;
lastWeaponInput = 0;
savedPlayerPos.resize(128);
savedPlayerFront.resize(128);
savedPlayerTeam.resize(128);
playerPosRecords.resize(128);
std::fill(savedPlayerTeam.begin(), savedPlayerTeam.end(), -1);
bandwidthMonitor.reset(new BandwidthMonitor(host));
}
NetClient::~NetClient() {
SPADES_MARK_FUNCTION();
Disconnect();
if (host)
enet_host_destroy(host);
bandwidthMonitor.reset();
SPLog("ENet host destroyed");
}
void NetClient::Connect(const ServerAddress &hostname) {
SPADES_MARK_FUNCTION();
Disconnect();
SPAssert(status == NetClientStatusNotConnected);
switch (hostname.GetProtocolVersion()) {
case ProtocolVersion::v075:
SPLog("Using Ace of Spades 0.75 protocol");
protocolVersion = 3;
break;
case ProtocolVersion::v076:
SPLog("Using Ace of Spades 0.76 protocol");
protocolVersion = 4;
break;
default: SPRaise("Invalid ProtocolVersion"); break;
}
ENetAddress addr = hostname.GetENetAddress();
SPLog("Connecting to %u:%u", (unsigned int)addr.host, (unsigned int)addr.port);
savedPackets.clear();
peer = enet_host_connect(host, &addr, 1, protocolVersion);
if (peer == NULL) {
SPRaise("Failed to create ENet peer");
}
properties.reset(new GameProperties(hostname.GetProtocolVersion()));
status = NetClientStatusConnecting;
statusString = _Tr("NetClient", "Connecting to the server");
}
void NetClient::Disconnect() {
SPADES_MARK_FUNCTION();
if (!peer)
return;
enet_peer_disconnect(peer, 0);
status = NetClientStatusNotConnected;
statusString = _Tr("NetClient", "Not connected");
savedPackets.clear();
ENetEvent event;
SPLog("Waiting for graceful disconnection");
while (enet_host_service(host, &event, 1000) > 0) {
switch (event.type) {
case ENET_EVENT_TYPE_RECEIVE: enet_packet_destroy(event.packet); break;
case ENET_EVENT_TYPE_DISCONNECT:
// disconnected safely
// FIXME: release peer
enet_peer_reset(peer);
peer = NULL;
return;
default:;
// discard
}
}
SPLog("Connection terminated");
enet_peer_reset(peer);
// FXIME: release peer
peer = NULL;
}
int NetClient::GetPing() {
SPADES_MARK_FUNCTION();
if (status == NetClientStatusNotConnected)
return -1;
auto rtt = peer->roundTripTime;
if (rtt == 0)
return -1;
return static_cast<int>(rtt);
}
void NetClient::DoEvents(int timeout) {
SPADES_MARK_FUNCTION();
if (status == NetClientStatusNotConnected)
return;
if (bandwidthMonitor)
bandwidthMonitor->Update();
ENetEvent event;
while (enet_host_service(host, &event, timeout) > 0) {
if (event.type == ENET_EVENT_TYPE_DISCONNECT) {
if (GetWorld()) {
client->SetWorld(NULL);
}
enet_peer_reset(peer);
peer = NULL;
status = NetClientStatusNotConnected;
SPLog("Disconnected (data = 0x%08x)", (unsigned int)event.data);
statusString = "Disconnected: " + DisconnectReasonString(event.data);
SPRaise("Disconnected: %s", DisconnectReasonString(event.data).c_str());
}
stmp::optional<NetPacketReader> readerOrNone;
if (event.type == ENET_EVENT_TYPE_RECEIVE) {
readerOrNone.reset(event.packet);
auto &reader = readerOrNone.value();
try {
if (HandleHandshakePackets(reader)) {
continue;
}
} catch (const std::exception &ex) {
int type = reader.GetType();
reader.DumpDebug();
SPRaise("Exception while handling packet type 0x%08x:\n%s", type,
ex.what());
}
}
if (status == NetClientStatusConnecting) {
if (event.type == ENET_EVENT_TYPE_CONNECT) {
statusString = _Tr("NetClient", "Awaiting for state");
} else if (event.type == ENET_EVENT_TYPE_RECEIVE) {
auto &reader = readerOrNone.value();
reader.DumpDebug();
if (reader.GetType() != PacketTypeMapStart) {
SPRaise("Unexpected packet: %d", (int)reader.GetType());
}
auto mapSize = reader.ReadInt();
SPLog("Map size advertised by the server: %lu", (unsigned long)mapSize);
mapLoader.reset(new GameMapLoader());
mapLoadMonitor.reset(new MapDownloadMonitor(*mapLoader));
status = NetClientStatusReceivingMap;
statusString = _Tr("NetClient", "Loading snapshot");
}
} else if (status == NetClientStatusReceivingMap) {
SPAssert(mapLoader);
if (event.type == ENET_EVENT_TYPE_RECEIVE) {
auto &reader = readerOrNone.value();
if (reader.GetType() == PacketTypeMapChunk) {
std::vector<char> dt = reader.GetData();
mapLoader->AddRawChunk(dt.data() + 1, dt.size() - 1);
mapLoadMonitor->AccumulateBytes(
static_cast<unsigned int>(dt.size() - 1));
} else {
reader.DumpDebug();
// The actual size of the map data cannot be known beforehand because
// of compression. This means we must detect the end of the map
// transfer in another way.
//
// We do this by checking for a StateData packet, which is sent
// directly after the map transfer completes.
//
// A number of other packets can also be received while loading the map:
//
// - World update packets (WorldUpdate, ExistingPlayer, and
// CreatePlayer) for the current round. We must store such packets
// temporarily and process them later when a `World` is created.
//
// - Leftover reload packet from the previous round. This happens when
// you initiate the reload action and a map change occurs before it
// is completed. In pyspades, sending a reload packet is implemented
// by registering a callback function to the Twisted reactor. This
// callback function sends a reload packet, but it does not check if
// the current game round is finished, nor is it unregistered on a
// map change.
//
// Such a reload packet would not (and should not) have any effect on
// the current round. Also, an attempt to process it would result in
// an "invalid player ID" exception, so we simply drop it during
// map load sequence.
//
if (reader.GetType() == PacketTypeStateData) {
status = NetClientStatusConnected;
statusString = _Tr("NetClient", "Connected");
try {
MapLoaded();
} catch (const std::exception &ex) {
if (strstr(ex.what(), "File truncated") ||
strstr(ex.what(), "EOF reached")) {
SPLog("Map decoder returned error:\n%s", ex.what());
Disconnect();
statusString = _Tr("NetClient", "Error");
throw;
}
} catch (...) {
Disconnect();
statusString = _Tr("NetClient", "Error");
throw;
}
HandleGamePacket(reader);
} else if (reader.GetType() == PacketTypeWeaponReload) {
// Drop the reload packet. Pyspades does not
// cancel the reload packets on map change and
// they would cause an error if we would
// process them
} else {
// Save the packet for later
savedPackets.push_back(reader.GetData());
}
}
}
} else if (status == NetClientStatusConnected) {
if (event.type == ENET_EVENT_TYPE_RECEIVE) {
auto &reader = readerOrNone.value();
// reader.DumpDebug();
try {
HandleGamePacket(reader);
} catch (const std::exception &ex) {
int type = reader.GetType();
reader.DumpDebug();
SPRaise("Exception while handling packet type 0x%08x:\n%s", type,
ex.what());
}
}
}
}
}
stmp::optional<World &> NetClient::GetWorld() { return client->GetWorld(); }
stmp::optional<Player &> NetClient::GetPlayerOrNull(int pId) {
SPADES_MARK_FUNCTION();
if (!GetWorld())
SPRaise("Invalid Player ID %d: No world", pId);
if (pId < 0 || pId >= GetWorld()->GetNumPlayerSlots())
return NULL;
return GetWorld()->GetPlayer(pId);
}
Player &NetClient::GetPlayer(int pId) {
SPADES_MARK_FUNCTION();
if (!GetWorld())
SPRaise("Invalid Player ID %d: No world", pId);
if (pId < 0 || pId >= GetWorld()->GetNumPlayerSlots())
SPRaise("Invalid Player ID %d: Out of range", pId);
if (!GetWorld()->GetPlayer(pId))
SPRaise("Invalid Player ID %d: Doesn't exist", pId);
return GetWorld()->GetPlayer(pId).value();
}
Player &NetClient::GetLocalPlayer() {
SPADES_MARK_FUNCTION();
if (!GetWorld())
SPRaise("Failed to get local player: no world");
if (!GetWorld()->GetLocalPlayer())
SPRaise("Failed to get local player: no local player");
return GetWorld()->GetLocalPlayer().value();
}
stmp::optional<Player &> NetClient::GetLocalPlayerOrNull() {
SPADES_MARK_FUNCTION();
if (!GetWorld())
SPRaise("Failed to get local player: no world");
return GetWorld()->GetLocalPlayer();
}
PlayerInput ParsePlayerInput(uint8_t bits) {
PlayerInput inp;
inp.moveForward = (bits & (1)) != 0;
inp.moveBackward = (bits & (1 << 1)) != 0;
inp.moveLeft = (bits & (1 << 2)) != 0;
inp.moveRight = (bits & (1 << 3)) != 0;
inp.jump = (bits & (1 << 4)) != 0;
inp.crouch = (bits & (1 << 5)) != 0;
inp.sneak = (bits & (1 << 6)) != 0;
inp.sprint = (bits & (1 << 7)) != 0;
return inp;
}
WeaponInput ParseWeaponInput(uint8_t bits) {
WeaponInput inp;
inp.primary = ((bits & (1)) != 0);
inp.secondary = ((bits & (1 << 1)) != 0);
return inp;
}
std::string NetClient::DisconnectReasonString(enet_uint32 num) {
switch (num) {
case 1: return _Tr("NetClient", "You are banned from this server.");
case 2:
// FIXME: this number seems to be used when per-IP connection limit was
// exceeded.
// we need to check other usages
return _Tr("NetClient", "You were kicked from this server.");
case 3: return _Tr("NetClient", "Incompatible client protocol version.");
case 4: return _Tr("NetClient", "Server full");
case 10: return _Tr("NetClient", "You were kicked from this server.");
default: return _Tr("NetClient", "Unknown Reason");
}
}
bool NetClient::HandleHandshakePackets(spades::client::NetPacketReader &reader) {
SPADES_MARK_FUNCTION();
switch (reader.GetType()) {
case PacketTypeHandShakeInit: SendHandShakeValid(reader.ReadInt()); return true;
case PacketTypeExtensionInfo: HandleExtensionPacket(reader); return true;
case PacketTypeVersionGet: {
if (reader.GetNumRemainingBytes() > 0) {
// Enhanced variant
std::set<std::uint8_t> propertyIds;
while (reader.GetNumRemainingBytes()) {
propertyIds.insert(reader.ReadByte());
}
SendVersionEnhanced(propertyIds);
} else {
// Simple variant
SendVersion();
}
}
return true;
default: return false;
}
}
void NetClient::HandleExtensionPacket(spades::client::NetPacketReader &reader) {
int ext_count = reader.ReadByte();
for (int i = 0; i < ext_count; i++) {
int ext_id = reader.ReadByte();
// int ext_version = reader.ReadByte();
auto got = implementedExtensions.find(ext_id);
if (got == implementedExtensions.end()) {
SPLog("Client does not support extension %d", ext_id);
} else {
SPLog("Client supports extension %d", ext_id);
extensions.emplace(got->first, got->second);
}
}
SendSupportedExtensions();
}
void NetClient::HandleGamePacket(spades::client::NetPacketReader &reader) {
SPADES_MARK_FUNCTION();
switch (reader.GetType()) {
case PacketTypePositionData: {
Player &p = GetLocalPlayer();
Vector3 pos;
if (reader.GetData().size() < 12) {
// sometimes 00 00 00 00 packet is sent.
// ignore this now
break;
}
pos.x = reader.ReadFloat();
pos.y = reader.ReadFloat();
pos.z = reader.ReadFloat();
p.SetPosition(pos);
} break;
case PacketTypeOrientationData: {
Player &p = GetLocalPlayer();
Vector3 pos;
pos.x = reader.ReadFloat();
pos.y = reader.ReadFloat();
pos.z = reader.ReadFloat();
p.SetOrientation(pos);
} break;
case PacketTypeWorldUpdate: {
// reader.DumpDebug();
int bytesPerEntry = 24;
if (protocolVersion == 4)
bytesPerEntry++;
client->MarkWorldUpdate();
int entries = static_cast<int>(reader.GetData().size() / bytesPerEntry);
for (int i = 0; i < entries; i++) {
int idx = i;
if (protocolVersion == 4) {
idx = reader.ReadByte();
if (idx < 0 || idx >= properties->GetMaxNumPlayerSlots()) {
SPRaise("Invalid player number %d received with WorldUpdate", idx);
}
}
Vector3 pos, front;
pos.x = reader.ReadFloat();
pos.y = reader.ReadFloat();
pos.z = reader.ReadFloat();
front.x = reader.ReadFloat();
front.y = reader.ReadFloat();
front.z = reader.ReadFloat();
savedPlayerPos.at(idx) = pos;
savedPlayerFront.at(idx) = front;
if (pos.x != 0.f || pos.y != 0.f || pos.z != 0.f || front.x != 0.f ||
front.y != 0.f || front.z != 0.f) {
SPAssert(!std::isnan(pos.x));
SPAssert(!std::isnan(pos.y));
SPAssert(!std::isnan(pos.z));
SPAssert(!std::isnan(front.x));
SPAssert(!std::isnan(front.y));
SPAssert(!std::isnan(front.z));
SPAssert(front.GetLength() < 40.f);
if (GetWorld()) {
auto p = GetWorld()->GetPlayer(idx);
if (p) {
if (p != GetWorld()->GetLocalPlayer()) {
p->SetPosition(pos);
p->SetOrientation(front);
PosRecord &rec = playerPosRecords[idx];
if (rec.valid) {
float timespan = GetWorld()->GetTime() - rec.time;
timespan = std::max(0.08f, timespan);
Vector3 vel = (pos - rec.pos) / timespan;
vel *= 1.f / 32.f;
p->SetVelocity(vel);
}
rec.valid = true;
rec.pos = pos;
rec.time = GetWorld()->GetTime();
}
}
}
}
}
SPAssert(reader.ReadRemainingData().empty());
} break;
case PacketTypeInputData:
if (!GetWorld())
break;
{
int pId = reader.ReadByte();
Player &p = GetPlayer(pId);
PlayerInput inp = ParsePlayerInput(reader.ReadByte());
if (GetWorld()->GetLocalPlayer() == &p) {
// handle "/fly" jump
if (inp.jump) {
p.ForceJump();
}
break;
}
p.SetInput(inp);
}
break;
case PacketTypeWeaponInput:
if (!GetWorld())
break;
{
int pId = reader.ReadByte();
Player &p = GetPlayer(pId);
WeaponInput inp = ParseWeaponInput(reader.ReadByte());
if (GetWorld()->GetLocalPlayer() == p)
break;
p.SetWeaponInput(inp);
}
break;
// Hit Packet is Client-to-Server!
case PacketTypeSetHP: {
Player &p = GetLocalPlayer();
int hp = reader.ReadByte();
int type = reader.ReadByte(); // 0=fall, 1=weap
Vector3 hurtPos;
hurtPos.x = reader.ReadFloat();
hurtPos.y = reader.ReadFloat();
hurtPos.z = reader.ReadFloat();
p.SetHP(hp, type ? HurtTypeWeapon : HurtTypeFall, hurtPos);
} break;
case PacketTypeGrenadePacket:
if (!GetWorld())
break;
{
reader.ReadByte(); // skip player Id
// Player *p = GetPlayerOrNull(reader.ReadByte());
float fuseLen = reader.ReadFloat();
Vector3 pos, vel;
pos.x = reader.ReadFloat();
pos.y = reader.ReadFloat();
pos.z = reader.ReadFloat();
vel.x = reader.ReadFloat();
vel.y = reader.ReadFloat();
vel.z = reader.ReadFloat();
/* blockpower mode may emit local player's grenade
if(p == GetLocalPlayerOrNull()){
// local player's grenade is already
// emit by Player
break;
}*/
Grenade *g = new Grenade(*GetWorld(), pos, vel, fuseLen);
GetWorld()->AddGrenade(std::unique_ptr<Grenade>{g});
}
break;
case PacketTypeSetTool: {
Player &p = GetPlayer(reader.ReadByte());
int tool = reader.ReadByte();
switch (tool) {
case 0: p.SetTool(Player::ToolSpade); break;
case 1: p.SetTool(Player::ToolBlock); break;
case 2: p.SetTool(Player::ToolWeapon); break;
case 3: p.SetTool(Player::ToolGrenade); break;
default: SPRaise("Received invalid tool type: %d", tool);
}
} break;
case PacketTypeSetColour: {
stmp::optional<Player &> p = GetPlayerOrNull(reader.ReadByte());
IntVector3 col = reader.ReadIntColor();
if (p)
p->SetHeldBlockColor(col);
else
temporaryPlayerBlockColor = col;
} break;
case PacketTypeExistingPlayer:
if (!GetWorld())
break;
{
int pId = reader.ReadByte();
int team = reader.ReadByte();
int weapon = reader.ReadByte();
int tool = reader.ReadByte();
int kills = reader.ReadInt();
IntVector3 color = reader.ReadIntColor();
std::string name = reader.ReadRemainingString();
// TODO: decode name?
WeaponType wType;
switch (weapon) {
case 0: wType = RIFLE_WEAPON; break;
case 1: wType = SMG_WEAPON; break;
case 2: wType = SHOTGUN_WEAPON; break;
default: SPRaise("Received invalid weapon: %d", weapon);
}
auto p = stmp::make_unique<Player>(*GetWorld(), pId, wType, team,
savedPlayerPos[pId],
GetWorld()->GetTeam(team).color);
p->SetHeldBlockColor(color);
// p->SetOrientation(savedPlayerFront[pId]);
switch (tool) {
case 0: p->SetTool(Player::ToolSpade); break;
case 1: p->SetTool(Player::ToolBlock); break;
case 2: p->SetTool(Player::ToolWeapon); break;
case 3: p->SetTool(Player::ToolGrenade); break;
default: SPRaise("Received invalid tool type: %d", tool);
}
GetWorld()->SetPlayer(pId, std::move(p));
World::PlayerPersistent &pers = GetWorld()->GetPlayerPersistent(pId);
pers.name = name;
pers.kills = kills;
savedPlayerTeam[pId] = team;
}
break;
case PacketTypeShortPlayerData: SPRaise("Unexpected: received Short Player Data");
case PacketTypeMoveObject:
if (!GetWorld())
SPRaise("No world");
{
uint8_t type = reader.ReadByte();
uint8_t state = reader.ReadByte();
Vector3 pos;
pos.x = reader.ReadFloat();
pos.y = reader.ReadFloat();
pos.z = reader.ReadFloat();
stmp::optional<IGameMode &> mode = GetWorld()->GetMode();
if (mode && IGameMode::m_CTF == mode->ModeType()) {
auto &ctf = dynamic_cast<CTFGameMode &>(mode.value());
switch (type) {
case BLUE_BASE: ctf.GetTeam(0).basePos = pos; break;
case BLUE_FLAG: ctf.GetTeam(0).flagPos = pos; break;
case GREEN_BASE: ctf.GetTeam(1).basePos = pos; break;
case GREEN_FLAG: ctf.GetTeam(1).flagPos = pos; break;
}
} else if (mode && IGameMode::m_TC == mode->ModeType()) {
auto &tc = dynamic_cast<TCGameMode &>(mode.value());
if (type >= tc.GetNumTerritories()) {
SPRaise("Invalid territory id specified: %d (max = %d)", (int)type,
tc.GetNumTerritories() - 1);
}
if (state > 2) {
SPRaise("Invalid state %d specified for territory owner.",
(int)state);
}
TCGameMode::Territory &t = tc.GetTerritory(type);
t.pos = pos;
t.ownerTeamId = state; /*
t->progressBasePos = 0.f;
t->progressRate = 0.f;
t->progressStartTime = 0.f;
t->capturingTeamId = -1;*/
}
}
break;
case PacketTypeCreatePlayer: {
if (!GetWorld())
SPRaise("No world");
int pId = reader.ReadByte();
int weapon = reader.ReadByte();
int team = reader.ReadByte();
Vector3 pos;
pos.x = reader.ReadFloat();
pos.y = reader.ReadFloat();
pos.z = reader.ReadFloat() - 2.f;
std::string name = reader.ReadRemainingString();
// TODO: decode name?
if (pId < 0 || pId >= properties->GetMaxNumPlayerSlots()) {
SPLog("Ignoring invalid player number %d (bug in pyspades?: %s)", pId,
name.c_str());
break;
}
WeaponType wType;
switch (weapon) {
case 0: wType = RIFLE_WEAPON; break;
case 1: wType = SMG_WEAPON; break;
case 2: wType = SHOTGUN_WEAPON; break;
default: SPRaise("Received invalid weapon: %d", weapon);
}
auto p =
stmp::make_unique<Player>(*GetWorld(), pId, wType, team, savedPlayerPos[pId],
GetWorld()->GetTeam(team).color);
p->SetPosition(pos);
GetWorld()->SetPlayer(pId, std::move(p));
Player &pRef = GetWorld()->GetPlayer(pId).value();
World::PlayerPersistent &pers = GetWorld()->GetPlayerPersistent(pId);
if (!name.empty()) // sometimes becomes empty
pers.name = name;
playerPosRecords[pId].valid = false;
if (pId == GetWorld()->GetLocalPlayerIndex()) {
client->LocalPlayerCreated();
lastPlayerInput = 0xffffffff;
lastWeaponInput = 0xffffffff;
SendHeldBlockColor(); // ensure block color synchronized
} else {
if (team < 2 && pId < (int)playerPosRecords.size()) {
PosRecord &rec = playerPosRecords[pId];
rec.valid = true;
rec.pos = pos;
rec.time = GetWorld()->GetTime();
}
if (savedPlayerTeam[pId] != team) {
client->PlayerJoinedTeam(pRef);
savedPlayerTeam[pId] = team;
}
}
client->PlayerSpawned(pRef);
} break;
case PacketTypeBlockAction: {
stmp::optional<Player &> p = GetPlayerOrNull(reader.ReadByte());
int action = reader.ReadByte();
IntVector3 pos;
pos.x = reader.ReadInt();
pos.y = reader.ReadInt();
pos.z = reader.ReadInt();
std::vector<IntVector3> cells;
if (action == 0) {
bool replace = GetWorld()->GetMap()->IsSolidWrapped(pos.x, pos.y, pos.z);
if (!p) {
GetWorld()->CreateBlock(pos, temporaryPlayerBlockColor);
} else {
GetWorld()->CreateBlock(pos, p->GetBlockColor());
client->PlayerCreatedBlock(*p);
if (!replace) {
p->UsedBlocks(1);
}
}
} else if (action == 1) {
cells.push_back(pos);
client->PlayerDestroyedBlockWithWeaponOrTool(pos);
GetWorld()->DestroyBlock(cells);
if (p && p->GetTool() == Player::ToolSpade) {
p->GotBlock();
}
} else if (action == 2) {
// dig
client->PlayerDiggedBlock(pos);
for (int z = -1; z <= 1; z++)
cells.push_back(IntVector3::Make(pos.x, pos.y, pos.z + z));
GetWorld()->DestroyBlock(cells);
} else if (action == 3) {
// grenade
client->GrenadeDestroyedBlock(pos);
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++)
for (int z = -1; z <= 1; z++)
cells.push_back(
IntVector3::Make(pos.x + x, pos.y + y, pos.z + z));
GetWorld()->DestroyBlock(cells);
}
} break;
case PacketTypeBlockLine: {
stmp::optional<Player &> p = GetPlayerOrNull(reader.ReadByte());
IntVector3 pos1, pos2;
pos1.x = reader.ReadInt();
pos1.y = reader.ReadInt();
pos1.z = reader.ReadInt();
pos2.x = reader.ReadInt();
pos2.y = reader.ReadInt();
pos2.z = reader.ReadInt();
IntVector3 col = p ? p->GetBlockColor() : temporaryPlayerBlockColor;
std::vector<IntVector3> cells;
cells = GetWorld()->CubeLine(pos1, pos2, 50);
for (size_t i = 0; i < cells.size(); i++) {
if (!GetWorld()->GetMap()->IsSolid(cells[i].x, cells[i].y, cells[i].z)) {
GetWorld()->CreateBlock(cells[i], col);
}
}
if (p) {
p->UsedBlocks(static_cast<int>(cells.size()));
client->PlayerCreatedBlock(*p);
}
} break;
case PacketTypeStateData:
if (!GetWorld())
break;
{
// receives my player info.
int pId = reader.ReadByte();
IntVector3 fogColor = reader.ReadIntColor();
IntVector3 teamColors[2];
teamColors[0] = reader.ReadIntColor();
teamColors[1] = reader.ReadIntColor();
std::string teamNames[2];
teamNames[0] = reader.ReadString(10);
teamNames[1] = reader.ReadString(10);
World::Team &t1 = GetWorld()->GetTeam(0);
World::Team &t2 = GetWorld()->GetTeam(1);
t1.color = teamColors[0];
t2.color = teamColors[1];
t1.name = teamNames[0];
t2.name = teamNames[1];
GetWorld()->SetFogColor(fogColor);
GetWorld()->SetLocalPlayerIndex(pId);
int mode = reader.ReadByte();
if (mode == 0) {
// CTF
auto mode = stmp::make_unique<CTFGameMode>();
CTFGameMode::Team &mt1 = mode->GetTeam(0);
CTFGameMode::Team &mt2 = mode->GetTeam(1);
mt1.score = reader.ReadByte();
mt2.score = reader.ReadByte();
mode->SetCaptureLimit(reader.ReadByte());
int intelFlags = reader.ReadByte();
mt1.hasIntel = (intelFlags & 1) != 0;
mt2.hasIntel = (intelFlags & 2) != 0;
if (mt2.hasIntel) {
mt1.carrier = reader.ReadByte();
reader.ReadData(11);
} else {
mt1.flagPos.x = reader.ReadFloat();
mt1.flagPos.y = reader.ReadFloat();
mt1.flagPos.z = reader.ReadFloat();
}
if (mt1.hasIntel) {
mt2.carrier = reader.ReadByte();
reader.ReadData(11);
} else {
mt2.flagPos.x = reader.ReadFloat();
mt2.flagPos.y = reader.ReadFloat();
mt2.flagPos.z = reader.ReadFloat();
}
mt1.basePos.x = reader.ReadFloat();
mt1.basePos.y = reader.ReadFloat();
mt1.basePos.z = reader.ReadFloat();
mt2.basePos.x = reader.ReadFloat();
mt2.basePos.y = reader.ReadFloat();
mt2.basePos.z = reader.ReadFloat();
GetWorld()->SetMode(std::move(mode));
} else {
// TC
auto mode = stmp::make_unique<TCGameMode>(*GetWorld());
int numTer = reader.ReadByte();
for (int i = 0; i < numTer; i++) {
TCGameMode::Territory ter{*mode};
ter.pos.x = reader.ReadFloat();
ter.pos.y = reader.ReadFloat();
ter.pos.z = reader.ReadFloat();
int state = reader.ReadByte();
ter.ownerTeamId = state;
ter.progressBasePos = 0.f;
ter.progressStartTime = 0.f;
ter.progressRate = 0.f;
ter.capturingTeamId = -1;
mode->AddTerritory(ter);
}
GetWorld()->SetMode(std::move(mode));
}
client->JoinedGame();
}
break;
case PacketTypeKillAction: {
Player &p = GetPlayer(reader.ReadByte());
Player *killer = &GetPlayer(reader.ReadByte());
int kt = reader.ReadByte();
KillType type;
switch (kt) {
case 0: type = KillTypeWeapon; break;
case 1: type = KillTypeHeadshot; break;
case 2: type = KillTypeMelee; break;
case 3: type = KillTypeGrenade; break;
case 4: type = KillTypeFall; break;
case 5: type = KillTypeTeamChange; break;
case 6: type = KillTypeClassChange; break;
default: SPInvalidEnum("kt", kt);
}
int respawnTime = reader.ReadByte();
switch (type) {
case KillTypeFall:
case KillTypeClassChange:
case KillTypeTeamChange: killer = &p; break;
default: break;
}
p.KilledBy(type, *killer, respawnTime);
if (&p != killer) {
GetWorld()->GetPlayerPersistent(killer->GetId()).kills += 1;
}
} break;
case PacketTypeChatMessage: {
// might be wrong player id for server message
uint8_t pId = reader.ReadByte();
stmp::optional<Player &> p = GetPlayerOrNull(pId);
int type = reader.ReadByte();
std::string txt = reader.ReadRemainingString();
if (p) {
switch (type) {
case 0: // all
client->PlayerSentChatMessage(*p, true, txt);
break;
case 1: // team
client->PlayerSentChatMessage(*p, false, txt);
break;
case 2: // system???
client->ServerSentMessage(txt);
}
} else {
client->ServerSentMessage(txt);
// Speculate the best game properties based on the server generated
// messages
properties->HandleServerMessage(txt);
}
} break;
case PacketTypeMapStart: {
// next map!
if (protocolVersion == 4) {
// The AoS 0.76 protocol allows the client to load a map from a local cache
// if possible. After receiving MapStart, the client should respond with
// MapCached to indicate whether the map with a given checksum exists in the
// cache or not. We don't implement a local cache, so we always ask the
// server to send fresh map data.
NetPacketWriter wri(PacketTypeMapCached);
wri.Write((uint8_t)0);
enet_peer_send(peer, 0, wri.CreatePacket());
}
client->SetWorld(NULL);
auto mapSize = reader.ReadInt();
SPLog("Map size advertised by the server: %lu", (unsigned long)mapSize);
mapLoader.reset(new GameMapLoader());
mapLoadMonitor.reset(new MapDownloadMonitor(*mapLoader));
status = NetClientStatusReceivingMap;
statusString = _Tr("NetClient", "Loading snapshot");
} break;
case PacketTypeMapChunk: SPRaise("Unexpected: received Map Chunk while game");
case PacketTypePlayerLeft: {
Player &p = GetPlayer(reader.ReadByte());
client->PlayerLeaving(p);
GetWorld()->GetPlayerPersistent(p.GetId()).kills = 0;
savedPlayerTeam[p.GetId()] = -1;
playerPosRecords[p.GetId()].valid = false;
GetWorld()->SetPlayer(p.GetId(), NULL);
// TODO: message
} break;
case PacketTypeTerritoryCapture: {
int territoryId = reader.ReadByte();
bool winning = reader.ReadByte() != 0;
int state = reader.ReadByte();
// TODO: This piece is repeated for at least three times
stmp::optional<IGameMode &> mode = GetWorld()->GetMode();
if (!mode) {
SPLog("Ignoring PacketTypeTerritoryCapture because game mode isn't "
"specified yet");
break;
}
if (mode->ModeType() != IGameMode::m_TC) {
SPRaise("Received PacketTypeTerritoryCapture in non-TC gamemode");
}
TCGameMode &tc = dynamic_cast<TCGameMode &>(*mode);
if (territoryId >= tc.GetNumTerritories()) {
SPRaise("Invalid territory id %d specified (max = %d)", territoryId,
tc.GetNumTerritories() - 1);
}
client->TeamCapturedTerritory(state, territoryId);
TCGameMode::Territory &t = tc.GetTerritory(territoryId);
t.ownerTeamId = state;
t.progressBasePos = 0.f;
t.progressRate = 0.f;
t.progressStartTime = 0.f;
t.capturingTeamId = -1;
if (winning)
client->TeamWon(state);
} break;
case PacketTypeProgressBar: {
int territoryId = reader.ReadByte();
int capturingTeam = reader.ReadByte();
int rate = (int8_t)reader.ReadByte();
float progress = reader.ReadFloat();
stmp::optional<IGameMode &> mode = GetWorld()->GetMode();
if (!mode) {
SPLog(
"Ignoring PacketTypeProgressBar because game mode isn't specified yet");
break;
}
if (mode->ModeType() != IGameMode::m_TC) {
SPRaise("Received PacketTypeProgressBar in non-TC gamemode");
}
TCGameMode &tc = dynamic_cast<TCGameMode &>(*mode);
if (territoryId >= tc.GetNumTerritories()) {
SPRaise("Invalid territory id %d specified (max = %d)", territoryId,
tc.GetNumTerritories() - 1);
}
if (progress < -0.1f || progress > 1.1f)
SPRaise("Progress value out of range(%f)", progress);
TCGameMode::Territory &t = tc.GetTerritory(territoryId);
t.progressBasePos = progress;
t.progressRate = (float)rate * TC_CAPTURE_RATE;
t.progressStartTime = GetWorld()->GetTime();
t.capturingTeamId = capturingTeam;
} break;
case PacketTypeIntelCapture: {
if (!GetWorld())
SPRaise("No world");
stmp::optional<IGameMode &> mode = GetWorld()->GetMode();
if (!mode) {
SPLog(
"Ignoring PacketTypeIntelCapture because game mode isn't specified yet");
break;
}
if (mode->ModeType() != IGameMode::m_CTF) {
SPRaise("Received PacketTypeIntelCapture in non-TC gamemode");
}
CTFGameMode &ctf = dynamic_cast<CTFGameMode &>(mode.value());
Player &p = GetPlayer(reader.ReadByte());
client->PlayerCapturedIntel(p);
GetWorld()->GetPlayerPersistent(p.GetId()).kills += 10;
ctf.GetTeam(p.GetTeamId()).hasIntel = false;
ctf.GetTeam(p.GetTeamId()).score++;
bool winning = reader.ReadByte() != 0;
if (winning) {
ctf.ResetTeamScoreAndIntelHoldingStatus();
client->TeamWon(p.GetTeamId());
}
} break;
case PacketTypeIntelPickup: {
Player &p = GetPlayer(reader.ReadByte());
stmp::optional<IGameMode &> mode = GetWorld()->GetMode();
if (!mode) {
SPLog(
"Ignoring PacketTypeIntelPickup because game mode isn't specified yet");
break;
}
if (mode->ModeType() != IGameMode::m_CTF) {
SPRaise("Received PacketTypeIntelPickup in non-TC gamemode");
}
CTFGameMode &ctf = dynamic_cast<CTFGameMode &>(mode.value());
CTFGameMode::Team &team = ctf.GetTeam(p.GetTeamId());
team.hasIntel = true;
team.carrier = p.GetId();
client->PlayerPickedIntel(p);
} break;
case PacketTypeIntelDrop: {
Player &p = GetPlayer(reader.ReadByte());
stmp::optional<IGameMode &> mode = GetWorld()->GetMode();
if (!mode) {
SPLog("Ignoring PacketTypeIntelDrop because game mode isn't specified yet");
break;
}
if (mode->ModeType() != IGameMode::m_CTF) {
SPRaise("Received PacketTypeIntelDrop in non-TC gamemode");
}
CTFGameMode &ctf = dynamic_cast<CTFGameMode &>(mode.value());
CTFGameMode::Team &team = ctf.GetTeam(p.GetTeamId());
team.hasIntel = false;
Vector3 pos;
pos.x = reader.ReadFloat();
pos.y = reader.ReadFloat();
pos.z = reader.ReadFloat();
ctf.GetTeam(1 - p.GetTeamId()).flagPos = pos;
client->PlayerDropIntel(p);
} break;
case PacketTypeRestock: {
Player &p = GetLocalPlayer(); // GetPlayer(reader.ReadByte());
p.Restock();
} break;
case PacketTypeFogColour: {
if (GetWorld()) {
reader.ReadByte(); // skip
GetWorld()->SetFogColor(reader.ReadIntColor());
}
} break;
case PacketTypeWeaponReload: {
Player &p = GetPlayer(reader.ReadByte());
if (&p != GetLocalPlayerOrNull())
p.Reload();
else {
int clip = reader.ReadByte();
int reserve = reader.ReadByte();
if (clip < 255 && reserve < 255) {
p.ReloadDone(clip, reserve);
}
}
// FIXME: use of "clip ammo" and "reserve ammo"?
} break;
case PacketTypeChangeTeam: {
Player &p = GetPlayer(reader.ReadByte());
int team = reader.ReadByte();
if (team < 0 || team > 2)
SPRaise("Received invalid team: %d", team);
p.SetTeam(team);
}
case PacketTypeChangeWeapon: {
reader.ReadByte();
WeaponType wType;
int weapon = reader.ReadByte();
switch (weapon) {
case 0: wType = RIFLE_WEAPON; break;
case 1: wType = SMG_WEAPON; break;
case 2: wType = SHOTGUN_WEAPON; break;
default: SPRaise("Received invalid weapon: %d", weapon);
}
// maybe this command is intended to change local player's
// weapon...
// p->SetWeaponType(wType);
} break;
default:
printf("WARNING: dropped packet %d\n", (int)reader.GetType());
reader.DumpDebug();
}
}
void NetClient::SendVersionEnhanced(const std::set<std::uint8_t> &propertyIds) {
NetPacketWriter wri(PacketTypeExistingPlayer);
wri.Write((uint8_t)'x');
for (std::uint8_t propertyId : propertyIds) {
wri.Write(propertyId);
auto lengthLabel = wri.GetPosition();
wri.Write((uint8_t)0); // dummy data for "Payload Length"
auto beginLabel = wri.GetPosition();
switch (static_cast<VersionInfoPropertyId>(propertyId)) {
case VersionInfoPropertyId::ApplicationNameAndVersion:
wri.Write((uint8_t)OpenSpades_VERSION_MAJOR);
wri.Write((uint8_t)OpenSpades_VERSION_MINOR);
wri.Write((uint8_t)OpenSpades_VERSION_REVISION);
wri.Write("OpenSpades");
break;
case VersionInfoPropertyId::UserLocale:
wri.Write(GetCurrentLocaleAndRegion());
break;
case VersionInfoPropertyId::ClientFeatureFlags1: {
auto flags = ClientFeatureFlags1::None;
if (cg_unicode) {
flags |= ClientFeatureFlags1::SupportsUnicode;
}
wri.Write(static_cast<uint32_t>(flags));
} break;
default:
// Just return empty payload for an unrecognized property
break;
}
wri.Update(lengthLabel, (uint8_t)(wri.GetPosition() - beginLabel));
enet_peer_send(peer, 0, wri.CreatePacket());
}
}
void NetClient::SendJoin(int team, WeaponType weapType, std::string name, int kills) {
SPADES_MARK_FUNCTION();
int weapId;
switch (weapType) {
case RIFLE_WEAPON: weapId = 0; break;
case SMG_WEAPON: weapId = 1; break;
case SHOTGUN_WEAPON: weapId = 2; break;
default: SPInvalidEnum("weapType", weapType);
}
NetPacketWriter wri(PacketTypeExistingPlayer);
wri.Write((uint8_t)0); // Player ID, but shouldn't matter here
wri.Write((uint8_t)team);
wri.Write((uint8_t)weapId);
wri.Write((uint8_t)2); // TODO: change tool
wri.Write((uint32_t)kills);
wri.WriteColor(GetWorld()->GetTeam(team).color);
wri.Write(name, 16);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendPosition() {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypePositionData);
// wri.Write((uint8_t)pId);
Player &p = GetLocalPlayer();
Vector3 v = p.GetPosition();
wri.Write(v.x);
wri.Write(v.y);
wri.Write(v.z);
enet_peer_send(peer, 0, wri.CreatePacket());
// printf("> (%f %f %f)\n", v.x, v.y, v.z);
}
void NetClient::SendOrientation(spades::Vector3 v) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeOrientationData);
// wri.Write((uint8_t)pId);
wri.Write(v.x);
wri.Write(v.y);
wri.Write(v.z);
enet_peer_send(peer, 0, wri.CreatePacket());
// printf("> (%f %f %f)\n", v.x, v.y, v.z);
}
void NetClient::SendPlayerInput(PlayerInput inp) {
SPADES_MARK_FUNCTION();
uint8_t bits = 0;
if (inp.moveForward)
bits |= 1 << 0;
if (inp.moveBackward)
bits |= 1 << 1;
if (inp.moveLeft)
bits |= 1 << 2;
if (inp.moveRight)
bits |= 1 << 3;
if (inp.jump)
bits |= 1 << 4;
if (inp.crouch)
bits |= 1 << 5;
if (inp.sneak)
bits |= 1 << 6;
if (inp.sprint)
bits |= 1 << 7;
if ((unsigned int)bits == lastPlayerInput)
return;
lastPlayerInput = bits;
NetPacketWriter wri(PacketTypeInputData);
wri.Write((uint8_t)GetLocalPlayer().GetId());
wri.Write(bits);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendWeaponInput(WeaponInput inp) {
SPADES_MARK_FUNCTION();
uint8_t bits = 0;
if (inp.primary)
bits |= 1 << 0;
if (inp.secondary)
bits |= 1 << 1;
if ((unsigned int)bits == lastWeaponInput)
return;
lastWeaponInput = bits;
NetPacketWriter wri(PacketTypeWeaponInput);
wri.Write((uint8_t)GetLocalPlayer().GetId());
wri.Write(bits);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendBlockAction(spades::IntVector3 v, BlockActionType type) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeBlockAction);
wri.Write((uint8_t)GetLocalPlayer().GetId());
switch (type) {
case BlockActionCreate: wri.Write((uint8_t)0); break;
case BlockActionTool: wri.Write((uint8_t)1); break;
case BlockActionDig: wri.Write((uint8_t)2); break;
case BlockActionGrenade: wri.Write((uint8_t)3); break;
default: SPInvalidEnum("type", type);
}
wri.Write((uint32_t)v.x);
wri.Write((uint32_t)v.y);
wri.Write((uint32_t)v.z);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendBlockLine(spades::IntVector3 v1, spades::IntVector3 v2) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeBlockLine);
wri.Write((uint8_t)GetLocalPlayer().GetId());
wri.Write((uint32_t)v1.x);
wri.Write((uint32_t)v1.y);
wri.Write((uint32_t)v1.z);
wri.Write((uint32_t)v2.x);
wri.Write((uint32_t)v2.y);
wri.Write((uint32_t)v2.z);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendReload() {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeWeaponReload);
wri.Write((uint8_t)GetLocalPlayer().GetId());
// these value should be 255, or
// NetClient will think reload was done when
// it receives echoed WeaponReload packet
wri.Write((uint8_t)255); // clip_ammo; not used?
wri.Write((uint8_t)255); // reserve_ammo; not used?
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendHeldBlockColor() {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeSetColour);
wri.Write((uint8_t)GetLocalPlayer().GetId());
IntVector3 v = GetLocalPlayer().GetBlockColor();
wri.WriteColor(v);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendTool() {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeSetTool);
wri.Write((uint8_t)GetLocalPlayer().GetId());
switch (GetLocalPlayer().GetTool()) {
case Player::ToolSpade: wri.Write((uint8_t)0); break;
case Player::ToolBlock: wri.Write((uint8_t)1); break;
case Player::ToolWeapon: wri.Write((uint8_t)2); break;
case Player::ToolGrenade: wri.Write((uint8_t)3); break;
default: SPInvalidEnum("tool", GetLocalPlayer().GetTool());
}
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendGrenade(const Grenade &g) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeGrenadePacket);
wri.Write((uint8_t)GetLocalPlayer().GetId());
wri.Write(g.GetFuse());
Vector3 v = g.GetPosition();
wri.Write(v.x);
wri.Write(v.y);
wri.Write(v.z);
v = g.GetVelocity();
wri.Write(v.x);
wri.Write(v.y);
wri.Write(v.z);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendHit(int targetPlayerId, HitType type) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeHitPacket);
wri.Write((uint8_t)targetPlayerId);
switch (type) {
case HitTypeTorso: wri.Write((uint8_t)0); break;
case HitTypeHead: wri.Write((uint8_t)1); break;
case HitTypeArms: wri.Write((uint8_t)2); break;
case HitTypeLegs: wri.Write((uint8_t)3); break;
case HitTypeMelee: wri.Write((uint8_t)4); break;
default: SPInvalidEnum("type", type);
}
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendChat(std::string text, bool global) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeChatMessage);
wri.Write((uint8_t)GetLocalPlayer().GetId());
wri.Write((uint8_t)(global ? 0 : 1));
wri.Write(text);
wri.Write((uint8_t)0);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendWeaponChange(WeaponType wt) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeChangeWeapon);
wri.Write((uint8_t)GetLocalPlayer().GetId());
switch (wt) {
case RIFLE_WEAPON: wri.Write((uint8_t)0); break;
case SMG_WEAPON: wri.Write((uint8_t)1); break;
case SHOTGUN_WEAPON: wri.Write((uint8_t)2); break;
}
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendTeamChange(int team) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeChangeTeam);
wri.Write((uint8_t)GetLocalPlayer().GetId());
wri.Write((uint8_t)team);
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendHandShakeValid(int challenge) {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeHandShakeReturn);
wri.Write((uint32_t)challenge);
SPLog("Sending hand shake back.");
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendVersion() {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeVersionSend);
wri.Write((uint8_t)'o');
wri.Write((uint8_t)OpenSpades_VERSION_MAJOR);
wri.Write((uint8_t)OpenSpades_VERSION_MINOR);
wri.Write((uint8_t)OpenSpades_VERSION_REVISION);
wri.Write(VersionInfo::GetVersionInfo());
SPLog("Sending version back.");
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::SendSupportedExtensions() {
SPADES_MARK_FUNCTION();
NetPacketWriter wri(PacketTypeExtensionInfo);
wri.Write(static_cast<uint8_t>(extensions.size()));
for (auto &i : extensions) {
wri.Write(static_cast<uint8_t>(i.first)); // ext id
wri.Write(static_cast<uint8_t>(i.second)); // ext version
}
SPLog("Sending extension support.");
enet_peer_send(peer, 0, wri.CreatePacket());
}
void NetClient::MapLoaded() {
SPADES_MARK_FUNCTION();
SPAssert(mapLoader);
// Move `mapLoader` to a local variable so that the associated resources
// are released as soon as possible when no longer needed
std::unique_ptr<GameMapLoader> mapLoader = std::move(this->mapLoader);
mapLoadMonitor.reset();
SPLog("Waiting for the game map decoding to complete...");
mapLoader->MarkEOF();
mapLoader->WaitComplete();
GameMap *map = mapLoader->TakeGameMap().Unmanage();
SPLog("The game map was decoded successfully.");
// now initialize world
World *w = new World(properties);
w->SetMap(map);
map->Release();
SPLog("World initialized.");
client->SetWorld(w);
SPAssert(GetWorld());
SPLog("World loaded. Processing saved packets (%d)...", (int)savedPackets.size());
for (size_t i = 0; i < playerPosRecords.size(); i++)
playerPosRecords[i].valid = false;
std::fill(savedPlayerTeam.begin(), savedPlayerTeam.end(), -1);
// do saved packets
try {
for (size_t i = 0; i < savedPackets.size(); i++) {
NetPacketReader r(savedPackets[i]);
HandleGamePacket(r);
}
savedPackets.clear();
SPLog("Done.");
} catch (...) {
savedPackets.clear();
throw;
}
}
float NetClient::GetMapReceivingProgress() {
SPAssert(status == NetClientStatusReceivingMap);
return mapLoader->GetProgress();
}
std::string NetClient::GetStatusString() {
if (status == NetClientStatusReceivingMap) {
// Display extra information
auto text = mapLoadMonitor->GetDisplayedText();
if (!text.empty()) {
return Format("{0} ({1})", statusString, text);
}
}
return statusString;
}
NetClient::BandwidthMonitor::BandwidthMonitor(ENetHost *host)
: host(host), lastDown(0.0), lastUp(0.0) {
sw.Reset();
}
void NetClient::BandwidthMonitor::Update() {
if (sw.GetTime() > 0.5) {
lastUp = host->totalSentData / sw.GetTime();
lastDown = host->totalReceivedData / sw.GetTime();
host->totalSentData = 0;
host->totalReceivedData = 0;
sw.Reset();
}
}
NetClient::MapDownloadMonitor::MapDownloadMonitor(GameMapLoader &mapLoader)
: numBytesDownloaded{0}, mapLoader{mapLoader}, receivedFirstByte{false} {}
void NetClient::MapDownloadMonitor::AccumulateBytes(unsigned int numBytes) {
// It might take a while before receiving the first byte. Take this into account to
// get a more accurate estimate of download time.
if (!receivedFirstByte) {
sw.Reset();
receivedFirstByte = true;
}
numBytesDownloaded += numBytes;
}
std::string NetClient::MapDownloadMonitor::GetDisplayedText() {
if (!receivedFirstByte) {
return {};
}
float secondsElapsed = static_cast<float>(sw.GetTime());
if (secondsElapsed <= 0.0) {
return {};
}
float progress = mapLoader.GetProgress();
float bytesPerSec = static_cast<float>(numBytesDownloaded) / secondsElapsed;
float progressPerSec = progress / secondsElapsed;
std::string text = Format("{0} KB, {1} KB/s", (numBytesDownloaded + 500) / 1000,
((int)bytesPerSec + 500) / 1000);
// Estimate the remaining time
float secondsRemaining = (1.0 - progress) / progressPerSec;
if (secondsRemaining < 86400.0) {
int seconds = (int)secondsRemaining + 1;
text += ", ";
if (seconds < 120) {
text +=
_TrN("NetClient", "{0} second remaining", "{0} seconds remaining", seconds);
} else {
text += _TrN("NetClient", "{0} minute remaining", "{0} minutes remaining",
seconds / 60);
}
}
return text;
}
} // namespace client
} // namespace spades