File: gns_client_connection.cpp

package info (click to toggle)
warzone2100 4.6.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 660,320 kB
  • sloc: cpp: 676,209; ansic: 391,201; javascript: 78,238; python: 16,632; php: 4,294; sh: 4,094; makefile: 2,629; lisp: 1,492; cs: 489; xml: 404; perl: 224; ruby: 156; java: 89
file content (208 lines) | stat: -rw-r--r-- 7,437 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// SPDX-License-Identifier: GPL-2.0-or-later

/*
	This file is part of Warzone 2100.
	Copyright (C) 2025  Warzone 2100 Project

	Warzone 2100 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 2 of the License, or
	(at your option) any later version.

	Warzone 2100 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 Warzone 2100; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "gns_client_connection.h"

#include "lib/framework/frame.h" // for ASSERT, ASSERT_OR_RETURN
#include "lib/netplay/descriptor_set.h"
#include "lib/netplay/error_categories.h"
#include "lib/netplay/gns/gns_error_categories.h"

#include <steam/isteamnetworkingsockets.h>
#include <steam/isteamnetworkingutils.h>
#include <steam/steamnetworkingtypes.h>

#include <cstring>
#include <algorithm>

namespace gns
{

GNSClientConnection::GNSClientConnection(WzConnectionProvider& connProvider, WzCompressionProvider& compressionProvider, PendingWritesManager& pwm,
		ISteamNetworkingSockets* networkInterface, HSteamNetConnection conn)
	: IClientConnection(connProvider, compressionProvider, pwm),
	networkInterface_(networkInterface),
	conn_(conn)
{
	ASSERT(isValid(), "Invalid GNS client connection handle");
}

GNSClientConnection::~GNSClientConnection()
{
	if (!isValid())
	{
		debug(LOG_WARNING, "~GNSClientConnection: expired connection handle");
		return;
	}
	// This is a best-effort attempt to flush the connection before closing it.
	// If it fails, we can still close the connection.
	flushPendingMessages();
	ASSERT(networkInterface_->CloseConnection(conn_, 0, nullptr, true) == true, "Failed to close client connection properly");
}

net::result<ssize_t> GNSClientConnection::sendImpl(const std::vector<uint8_t>& data)
{
	ASSERT_OR_RETURN(tl::make_unexpected(make_gns_error_code(EINVAL)), isValid(), "Invalid GNS client connection handle");

	int sendFlags = k_nSteamNetworkingSend_Reliable;
	if (!useNagle_)
	{
		sendFlags |= k_nSteamNetworkingSend_NoNagle;
	}
	// Limit the size of the message to the maximum size allowed by the GNS library.
	// Higher-level code will automatically handle this case and split the payload into several messages if needed.
	const auto size = std::min(data.size(), static_cast<size_t>(k_cbMaxSteamNetworkingSocketsMessageSizeSend));
	auto res = networkInterface_->SendMessageToConnection(conn_, data.data(), static_cast<uint32_t>(size), sendFlags, nullptr);
	if (res != k_EResultOK)
	{
		return tl::make_unexpected(make_gns_error_code(res));
	}
	return size;
}

net::result<ssize_t> GNSClientConnection::recvImpl(char* dst, size_t maxSize)
{
	ASSERT_OR_RETURN(tl::make_unexpected(make_gns_error_code(EINVAL)), isValid(), "Invalid GNS client connection handle");
	ASSERT_OR_RETURN(0, readReady(), "No data to read from the connection");

	size_t currentProcessedSize = 0;
	while (!pendingMessagesToRead_.empty() && currentProcessedSize < maxSize)
	{
		SteamNetworkingMessage_t* msg = pendingMessagesToRead_.front();

		// Determine how much bytes left to read from the top of the queue.
		size_t bytesToRead = static_cast<size_t>(msg->m_cbSize) - currentMsgReadPos_;
		if (bytesToRead > maxSize)
		{
			// If there's more than `maxSize`, just copy the max size bytes and adjust the offset accordingly.
			std::memcpy(dst, reinterpret_cast<const char*>(msg->m_pData) + currentMsgReadPos_, maxSize);
			currentMsgReadPos_ += maxSize;
			return maxSize;
		}
		// Consume the current message and dispose of it.
		pendingMessagesToRead_.pop();
		std::memcpy(dst, reinterpret_cast<const char*>(msg->m_pData) + currentMsgReadPos_, bytesToRead);
		dst += bytesToRead;
		currentProcessedSize += bytesToRead;
		// Reset the read position for the next message.
		currentMsgReadPos_ = 0;

		msg->Release();
	}
	return currentProcessedSize;
}

void GNSClientConnection::useNagleAlgorithm(bool enable)
{
	useNagle_ = enable;
}

std::string GNSClientConnection::textAddress() const
{
	SteamNetConnectionInfo_t connInfo;
	if (!networkInterface_->GetConnectionInfo(conn_, &connInfo))
	{
		return {};
	}
	char buf[SteamNetworkingIdentity::k_cchMaxString] = {};

	switch (connInfo.m_identityRemote.m_eType)
	{
		case k_ESteamNetworkingIdentityType_IPAddress:
		{
			// Special handling of the ip address case
			// Return the ip address (without the "ip:" prefix that SteamNetworkingIdentity::ToString would add, and without the port), to match TCP backend behavior
			auto pIPAddr = connInfo.m_identityRemote.GetIPAddr();
			ASSERT_OR_RETURN({}, pIPAddr != nullptr, "Unable to get ip address?");
			pIPAddr->ToString(buf, SteamNetworkingIdentity::k_cchMaxString, false);
			return std::string(buf);
		}
		case k_ESteamNetworkingIdentityType_Invalid:
			ASSERT(false, "Connection identity type is invalid?");
			return {};
		default:
			// Other cases - use SteamNetworkingIdentity::ToString, which returns a string with a "prefix:" indicating what type of identity it is
			connInfo.m_identityRemote.ToString(buf, SteamNetworkingIdentity::k_cchMaxString);
			return std::string(buf);
	}
	return {}; // silence compiler warning
}

bool GNSClientConnection::isValid() const
{
	return networkInterface_ != nullptr && conn_ != k_HSteamNetConnection_Invalid;
}

net::result<void> GNSClientConnection::connectionStatus() const
{
	ASSERT_OR_RETURN(tl::make_unexpected(make_network_error_code(EINVAL)), isValid(), "Invalid GNS network interface pointer");

	SteamNetConnectionRealTimeStatus_t status;
	auto res = networkInterface_->GetConnectionRealTimeStatus(conn_, &status, 0, nullptr);
	if (res != k_EResultOK)
	{
		return tl::make_unexpected(make_gns_error_code(res));
	}
	if (status.m_eState != k_ESteamNetworkingConnectionState_Connected)
	{
		return tl::make_unexpected(make_gns_error_code(k_EResultNoConnection));
	}
	return {};
}

void GNSClientConnection::setConnectedTimeout(std::chrono::milliseconds timeout)
{
	ASSERT_OR_RETURN(, isValid(), "Invalid GNS client connection handle");
	ASSERT(SteamNetworkingUtils()->SetConnectionConfigValueInt32(conn_, k_ESteamNetworkingConfig_TimeoutConnected, static_cast<int32_t>(timeout.count())),
		"Failed to set connected timeout for connection %u", conn_);
}

void GNSClientConnection::enqueueMessage(SteamNetworkingMessage_t* msg)
{
	pendingMessagesToRead_.push(msg);
}

void GNSClientConnection::flushPendingMessages()
{
	ASSERT_OR_RETURN(, isValid(), "Invalid GNS client connection handle");

	const auto flushRes = networkInterface_->FlushMessagesOnConnection(conn_);
	if (flushRes != k_EResultOK && flushRes != k_EResultIgnored)
	{
		const auto errCode = make_gns_error_code(flushRes);
		const auto errMsg = errCode.message();
		debug(LOG_WARNING, "Failed to flush messages on connection: %s", errMsg.c_str());
	}

	// Discard any pending messages, that weren't yet processed by this connection object
	for (; !pendingMessagesToRead_.empty(); pendingMessagesToRead_.pop())
	{
		pendingMessagesToRead_.front()->Release();
	}
}

void GNSClientConnection::expireConnectionHandle()
{
	conn_ = k_HSteamNetConnection_Invalid;
}


} // namespace gns