/* 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 . */ #include #include #include #include #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 #include #include #include #include #include #include #include #include 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 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 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 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 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 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(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(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(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 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 dt = reader.GetData(); mapLoader->AddRawChunk(dt.data() + 1, dt.size() - 1); mapLoadMonitor->AccumulateBytes( static_cast(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 NetClient::GetWorld() { return client->GetWorld(); } stmp::optional 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 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 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(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.16f, 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{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 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(*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 mode = GetWorld()->GetMode(); if (mode && IGameMode::m_CTF == mode->ModeType()) { auto &ctf = dynamic_cast(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(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(*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 p = GetPlayerOrNull(reader.ReadByte()); int action = reader.ReadByte(); IntVector3 pos; pos.x = reader.ReadInt(); pos.y = reader.ReadInt(); pos.z = reader.ReadInt(); std::vector 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 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 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(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::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(*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 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! 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 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(*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 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(*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 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(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) client->TeamWon(p.GetTeamId()); } break; case PacketTypeIntelPickup: { Player &p = GetPlayer(reader.ReadByte()); stmp::optional 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(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 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(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 &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(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(flags)); } break; default: // Just return empty payload for an unrecognized property break; } wri.Update(lengthLabel, (uint8_t)(wri.GetPosition() - beginLabel)); } } 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(extensions.size())); for (auto &i : extensions) { wri.Write(static_cast(i.first)); // ext id wri.Write(static_cast(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 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(sw.GetTime()); if (secondsElapsed <= 0.0) { return {}; } float progress = mapLoader.GetProgress(); float bytesPerSec = static_cast(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