m_itemdef_received(false),
m_nodedef_received(false),
m_media_downloader(new ClientMediaDownloader()),
+ m_last_action_seq(0),
m_time_of_day_set(false),
m_last_time_of_day_f(-1),
m_time_of_day_update_timer(0),
m_con.Send(PEER_ID_SERVER, 1, reply, true);
m_state = LC_Init;
+ m_last_action_seq = 0;
+ m_node_predictions.clear();
return;
}
p.X = readS16(&data[2]);
p.Y = readS16(&data[4]);
p.Z = readS16(&data[6]);
+ if(datasize >= 10)
+ {
+ for(RingBuffer<NodePrediction, NODE_PREDICTION_QUEUE_CAPACITY>::iterator
+ i = firstPendingPrediction(readU16(&data[8])),
+ ie = m_node_predictions.end();
+ i != ie; ++i) {
+ if(i->pos == p) {
+ // notification from server is
+ // superseded by a later prediction
+ return;
+ }
+ }
+ }
removeNode(p);
}
else if(command == TOCLIENT_ADDNODE)
remove_metadata = false;
}
+ if(datasize >= index+3)
+ {
+ for(RingBuffer<NodePrediction, NODE_PREDICTION_QUEUE_CAPACITY>::iterator
+ i = firstPendingPrediction(readU16(&data[index+1])),
+ ie = m_node_predictions.end();
+ i != ie; ++i) {
+ if(i->pos == p) {
+ // notification from server is
+ // superseded by a later prediction
+ return;
+ }
+ }
+ }
+
addNode(p, n, remove_metadata);
}
else if(command == TOCLIENT_BLOCKDATA)
sector->insertBlock(block);
}
+ try
+ {
+ v3s16 brpos = block->getPosRelative();
+ for(RingBuffer<NodePrediction, NODE_PREDICTION_QUEUE_CAPACITY>::iterator
+ i = firstPendingPrediction(readU16(istr)),
+ ie = m_node_predictions.end();
+ i != ie; ++i) {
+ // replay prediction if it applies to this block
+ v3s16 bipos = i->pos - brpos;
+ if(block->isValidPosition(bipos))
+ block->setNodeNoCheck(bipos, i->node);
+ }
+ }
+ catch(SerializationError& e) {}
+
/*
Add it to mesh update queue and set it to be acknowledged after update.
*/
m_con.Send(PEER_ID_SERVER, channelnum, data, reliable);
}
+void Client::incrementActionSeq()
+{
+ m_last_action_seq++;
+ while(!m_node_predictions.empty() &&
+ m_node_predictions.front().action_seq == m_last_action_seq)
+ m_node_predictions.pop_front();
+}
+
+RingBuffer<NodePrediction, NODE_PREDICTION_QUEUE_CAPACITY>::iterator
+Client::firstPendingPrediction(u16 last_action_considered)
+{
+ RingBuffer<NodePrediction, NODE_PREDICTION_QUEUE_CAPACITY>::iterator
+ i = m_node_predictions.end(), ib = m_node_predictions.begin();
+ u16 how_far_back = m_last_action_seq - last_action_considered;
+ while(i != ib) {
+ --i;
+ if(m_last_action_seq - i->action_seq >= how_far_back) {
+ ++i;
+ break;
+ }
+ }
+ return i;
+}
+
void Client::interact(u8 action, const PointedThing& pointed)
{
if(m_state != LC_Ready){
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::sendNodemetaFields(v3s16 p, const std::string &formname,
SharedBuffer<u8> data((u8*)s.c_str(), s.size());
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::sendInventoryFields(const std::string &formname,
SharedBuffer<u8> data((u8*)s.c_str(), s.size());
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::sendInventoryAction(InventoryAction *a)
SharedBuffer<u8> data((u8*)s.c_str(), s.size());
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::sendChatMessage(const std::wstring &message)
SharedBuffer<u8> data((u8*)s.c_str(), s.size());
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::sendChangePassword(const std::wstring &oldpassword,
SharedBuffer<u8> data((u8*)s.c_str(), s.size());
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
SharedBuffer<u8> data((u8*)s.c_str(), s.size());
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::sendBreath(u16 breath)
SharedBuffer<u8> data((u8*)s.c_str(), s.size());
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::sendRespawn()
SharedBuffer<u8> data((u8*)s.c_str(), s.size());
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::sendReady()
// Send as reliable
Send(0, data, true);
+
+ incrementActionSeq();
}
void Client::removeNode(v3s16 p)
addUpdateMeshTaskWithEdge(i->first);
}
}
+
+void Client::notePredictedNode(v3s16 p)
+{
+ try {
+ m_node_predictions.push_back(NodePrediction(m_last_action_seq,
+ p, m_env.getMap().getNode(p)));
+ } catch(InvalidPositionException &e) {}
+}
void Client::setPlayerControl(PlayerControl &control)
{
#include "localplayer.h"
#include "hud.h"
#include "particles.h"
+#include "util/ringbuffer.h"
struct MeshMakeData;
class MapBlockMesh;
std::map<u16, u16> m_packets;
};
+/*
+ node operation predictions
+
+ The client maintains a ring buffer recording predictions that it's
+ made for changes to nodes. action_seq identifies which packet
+ sent by the client should cause the change to occur on the server.
+ (The sequence number is just the count of preceding game-action
+ packets sent by the client since TOSERVER_INIT2, modulo 2**16;
+ client and server both keep count.) When the server sends the
+ client a packet with node state updates, it also indicates which
+ action packet it most recently saw from the client, and hence
+ which client actions have been taken into account in producing the
+ node update, and hence which client predictions are superseded
+ by the update. The client then checks for predictions that are
+ relevant to the update and have not been superseded by it, and
+ replays them on top of the updated state, in order to preserve
+ the prediction until the server has processed the action.
+
+ A prediction is dropped from the buffer if the buffer overflows
+ (by having too many predictions pending), or if sequence numbers
+ have nearly wrapped such that its action_seq would be misleading.
+ Dropping the record of a prediction means that the predicted node
+ state might be overwritten by an update from the server that
+ doesn't reflect the pending action; this produces a glitch but
+ eventually self-heals when the server catches up. Normally,
+ by the time a prediction is dropped the server has long ago
+ handled the action, so these glitches are avoided.
+*/
+struct NodePrediction {
+ u16 action_seq;
+ v3s16 pos;
+ MapNode node;
+ NodePrediction(const NodePrediction &v) { *this = v; }
+ NodePrediction(u16 seq, v3s16 p, MapNode n)
+ : action_seq(seq), pos(p), node(n) {}
+ NodePrediction() : action_seq(0), pos(), node() {}
+};
+enum { NODE_PREDICTION_QUEUE_CAPACITY = 0x3ff };
+
class Client : public con::PeerHandler, public InventoryManager, public IGameDef
{
public:
// Causes urgent mesh updates (unlike Map::add/removeNodeWithEvent)
void removeNode(v3s16 p);
void addNode(v3s16 p, MapNode n, bool remove_metadata = true);
+ void notePredictedNode(v3s16 p);
void setPlayerControl(PlayerControl &control);
void sendPlayerPos();
// Send the item number 'item' as player item to the server
void sendPlayerItem(u16 item);
+
+ void incrementActionSeq();
+ RingBuffer<NodePrediction, NODE_PREDICTION_QUEUE_CAPACITY>::iterator
+ firstPendingPrediction(u16 last_action_considered);
float m_packetcounter_timer;
float m_connection_reinit_timer;
bool m_itemdef_received;
bool m_nodedef_received;
ClientMediaDownloader *m_media_downloader;
+ u16 m_last_action_seq;
+ RingBuffer<NodePrediction, NODE_PREDICTION_QUEUE_CAPACITY> m_node_predictions;
// time_of_day speed approximation for old protocol
bool m_time_of_day_set;
u8 serialization_version;
//
u16 net_proto_version;
+ // sequence number of last game action seen from client
+ u16 last_action_seq;
RemoteClient():
peer_id(PEER_ID_INEXISTENT),
serialization_version(SER_FMT_VER_INVALID),
net_proto_version(0),
+ last_action_seq(0),
m_time_from_building(9999),
m_pending_serialization_version(SER_FMT_VER_INVALID),
m_state(CS_Created),
*/
TOCLIENT_BLOCKDATA = 0x20, //TODO: Multiple blocks
+ /*
+ u16 command
+ v3s16 position
+ serialized block
+ u16 last_action_seq // Added in protocol version 23
+ */
TOCLIENT_ADDNODE = 0x21,
/*
u16 command
v3s16 position
serialized mapnode
u8 keep_metadata // Added in protocol version 22
+ u16 last_action_seq // Added in protocol version 23
*/
TOCLIENT_REMOVENODE = 0x22,
+ /*
+ u16 command
+ v3s16 position
+ u16 last_action_seq // Added in protocol version 23
+ */
TOCLIENT_PLAYERPOS = 0x23, // Obsolete
/*
};
bool nodePlacementPrediction(Client &client,
- const ItemDefinition &playeritem_def, v3s16 nodepos, v3s16 neighbourpos)
+ const ItemDefinition &playeritem_def,
+ v3s16 nodepos, v3s16 neighbourpos, v3s16 *predicted_pos_p)
{
std::string prediction = playeritem_def.node_placement_prediction;
INodeDefManager *nodedef = client.ndef();
// This triggers the required mesh update too
client.addNode(p, n);
+ *predicted_pos_p = p;
return true;
}
}catch(InvalidPositionException &e){
client.setCrack(-1, v3s16(0,0,0));
MapNode wasnode = map.getNode(nodepos);
client.removeNode(nodepos);
+ client.notePredictedNode(nodepos);
if (g_settings->getBool("enable_particles"))
{
// If the wielded item has node placement prediction,
// make that happen
+ v3s16 predicted_pos;
bool placed = nodePlacementPrediction(client,
playeritem_def,
- nodepos, neighbourpos);
+ nodepos, neighbourpos,
+ &predicted_pos);
if(placed) {
// Report to server
client.interact(3, pointed);
+ client.notePredictedNode(predicted_pos);
// Read the sound
soundmaker.m_player_rightpunch_sound =
playeritem_def.sound_place;
verbosestream<<"Server: Got TOSERVER_INIT2 from "
<<peer_id<<std::endl;
+ RemoteClient* client = getClient(peer_id, CS_Created);
+
+ client->last_action_seq = 0;
+
m_clients.event(peer_id, CSE_GotInit2);
u16 protocol_version = m_clients.getProtocolVersion(peer_id);
///// end compatibility code
// Warnings about protocol version can be issued here
- if(getClient(peer_id)->net_proto_version < LATEST_PROTOCOL_VERSION)
+ if(client->net_proto_version < LATEST_PROTOCOL_VERSION)
{
SendChatMessage(peer_id, L"# Server: WARNING: YOUR CLIENT'S "
L"VERSION MAY NOT BE FULLY COMPATIBLE WITH THIS SERVER!");
}
else if(command == TOSERVER_INVENTORY_ACTION)
{
+ getClient(peer_id)->last_action_seq++;
// Strip command and create a stream
std::string datastring((char*)&data[2], datasize-2);
verbosestream<<"TOSERVER_INVENTORY_ACTION: data="<<datastring<<std::endl;
}
else if(command == TOSERVER_CHAT_MESSAGE)
{
+ getClient(peer_id)->last_action_seq++;
/*
u16 command
u16 length
}
else if(command == TOSERVER_DAMAGE)
{
+ getClient(peer_id)->last_action_seq++;
std::string datastring((char*)&data[2], datasize-2);
std::istringstream is(datastring, std::ios_base::binary);
u8 damage = readU8(is);
}
else if(command == TOSERVER_BREATH)
{
+ getClient(peer_id)->last_action_seq++;
std::string datastring((char*)&data[2], datasize-2);
std::istringstream is(datastring, std::ios_base::binary);
u16 breath = readU16(is);
}
else if(command == TOSERVER_PASSWORD)
{
+ getClient(peer_id)->last_action_seq++;
/*
[0] u16 TOSERVER_PASSWORD
[2] u8[28] old password
}
else if(command == TOSERVER_PLAYERITEM)
{
+ getClient(peer_id)->last_action_seq++;
if (datasize < 2+2)
return;
}
else if(command == TOSERVER_RESPAWN)
{
+ getClient(peer_id)->last_action_seq++;
if(player->hp != 0 || !g_settings->getBool("enable_damage"))
return;
}
else if(command == TOSERVER_INTERACT)
{
+ getClient(peer_id)->last_action_seq++;
std::string datastring((char*)&data[2], datasize-2);
std::istringstream is(datastring, std::ios_base::binary);
}
else if(command == TOSERVER_NODEMETA_FIELDS)
{
+ getClient(peer_id)->last_action_seq++;
std::string datastring((char*)&data[2], datasize-2);
std::istringstream is(datastring, std::ios_base::binary);
}
else if(command == TOSERVER_INVENTORY_FIELDS)
{
+ getClient(peer_id)->last_action_seq++;
std::string datastring((char*)&data[2], datasize-2);
std::istringstream is(datastring, std::ios_base::binary);
float maxd = far_d_nodes*BS;
v3f p_f = intToFloat(p, BS);
- // Create packet
- u32 replysize = 8;
- SharedBuffer<u8> reply(replysize);
- writeU16(&reply[0], TOCLIENT_REMOVENODE);
- writeS16(&reply[2], p.X);
- writeS16(&reply[4], p.Y);
- writeS16(&reply[6], p.Z);
-
std::list<u16> clients = m_clients.getClientIDs();
for(std::list<u16>::iterator
i = clients.begin();
}
}
+ SharedBuffer<u8> reply(0);
+ m_clients.Lock();
+ RemoteClient* client = m_clients.lockedGetClientNoEx(*i);
+ if (client != 0)
+ {
+ // Create packet
+ u32 replysize = client->net_proto_version >= 23 ? 10 : 8;
+ SharedBuffer<u8> reply(replysize);
+ writeU16(&reply[0], TOCLIENT_REMOVENODE);
+ writeS16(&reply[2], p.X);
+ writeS16(&reply[4], p.Y);
+ writeS16(&reply[6], p.Z);
+ if (client->net_proto_version >= 23)
+ writeU16(&reply[8], client->last_action_seq);
+ }
+ m_clients.Unlock();
+
// Send as reliable
- m_clients.send(*i, 0, reply, true);
+ if (reply.getSize() > 0)
+ m_clients.send(*i, 0, reply, true);
}
}
{
// Create packet
u32 replysize = 9 + MapNode::serializedLength(client->serialization_version);
+ if (client->net_proto_version >= 23)
+ replysize += 2;
reply = SharedBuffer<u8>(replysize);
writeU16(&reply[0], TOCLIENT_ADDNODE);
writeS16(&reply[2], p.X);
n.serialize(&reply[8], client->serialization_version);
u32 index = 8 + MapNode::serializedLength(client->serialization_version);
writeU8(&reply[index], remove_metadata ? 0 : 1);
+ if (client->net_proto_version >= 23)
+ writeU16(&reply[index+1], client->last_action_seq);
if (!remove_metadata) {
if (client->net_proto_version <= 21) {
m_clients.Unlock();
}
-void Server::SendBlockNoLock(u16 peer_id, MapBlock *block, u8 ver, u16 net_proto_version)
+void Server::SendBlockNoLock(u16 peer_id, MapBlock *block,
+ u8 ver, u16 net_proto_version, u16 last_action_seq)
{
DSTACK(__FUNCTION_NAME);
SharedBuffer<u8> blockdata((u8*)s.c_str(), s.size());
u32 replysize = 8 + blockdata.getSize();
+ if (net_proto_version >= 23)
+ replysize += 2;
SharedBuffer<u8> reply(replysize);
writeU16(&reply[0], TOCLIENT_BLOCKDATA);
writeS16(&reply[2], p.X);
writeS16(&reply[4], p.Y);
writeS16(&reply[6], p.Z);
memcpy(&reply[8], *blockdata, blockdata.getSize());
+ if (net_proto_version >= 23)
+ writeU16(&reply[8 + blockdata.getSize()], last_action_seq);
/*infostream<<"Server: Sending block ("<<p.X<<","<<p.Y<<","<<p.Z<<")"
<<": \tpacket size: "<<replysize<<std::endl;*/
if(!client)
continue;
- SendBlockNoLock(q.peer_id, block, client->serialization_version, client->net_proto_version);
+ SendBlockNoLock(q.peer_id, block, client->serialization_version, client->net_proto_version, client->last_action_seq);
client->SentBlock(q.pos);
total_sending++;
void setBlockNotSent(v3s16 p);
// Environment and Connection must be locked when called
- void SendBlockNoLock(u16 peer_id, MapBlock *block, u8 ver, u16 net_proto_version);
+ void SendBlockNoLock(u16 peer_id, MapBlock *block, u8 ver, u16 net_proto_version, u16 last_action_seq);
// Sends blocks to clients (locks env and con on its own)
void SendBlocks(float dtime);
--- /dev/null
+#ifndef UTIL_RINGBUFFER_HEADER
+#define UTIL_RINGBUFFER_HEADER
+
+#include <cstddef>
+
+// Ring buffer container (partial). This is intended for very lightweight
+// value types, for which dynamically allocating storage would be
+// noticeably expensive. The ring buffer has a statically-determined
+// capacity, and allocates in one go all the storage needed for the full
+// number of values. Items can be pushed on the back of the buffer and
+// popped off the front very cheaply. If more items are pushed than the
+// predeclared capacity, then items are lost from the front. Thus this
+// can't be used where reliable storage is required; it should be used
+// where limiting the memory usage is more important than retaining
+// the information.
+
+template <typename VALUE_TYPE, size_t CAPACITY> class RingBuffer {
+public:
+ typedef VALUE_TYPE value_type;
+ typedef VALUE_TYPE &reference;
+ typedef const VALUE_TYPE &const_reference;
+ typedef VALUE_TYPE *pointer;
+ typedef const VALUE_TYPE *const_pointer;
+ typedef size_t size_type;
+private:
+ size_type front_pos, back_pos;
+ value_type elems[CAPACITY+1];
+public:
+ RingBuffer() : front_pos(0), back_pos(0) {}
+ size_type size() const {
+ size_type s = back_pos - front_pos;
+ if (back_pos < front_pos)
+ s += CAPACITY+1;
+ return s;
+ }
+ size_type max_size() const { return CAPACITY; }
+ bool empty() const { return back_pos == front_pos; }
+ reference front() { return elems[front_pos]; }
+ const_reference front() const { return elems[front_pos]; }
+ reference back() { return elems[back_pos-1]; }
+ const_reference back() const { return elems[back_pos-1]; }
+ reference operator[](size_type n) {
+ size_type i = front_pos + n;
+ if (i >= CAPACITY+1)
+ i -= CAPACITY+1;
+ return elems[i];
+ }
+ const_reference operator[](size_type n) const {
+ size_type i = front_pos + n;
+ if (i >= CAPACITY+1)
+ i -= CAPACITY+1;
+ return elems[i];
+ }
+ void pop_front() {
+ front_pos++;
+ if (front_pos == CAPACITY+1) {
+ front_pos = 0;
+ if (back_pos == CAPACITY+1)
+ back_pos = 0;
+ }
+ }
+ void push_back(const value_type &val) {
+ if (back_pos == CAPACITY+1)
+ back_pos = 0;
+ elems[back_pos++] = val;
+ if (front_pos == (back_pos == CAPACITY+1 ? 0 : back_pos))
+ pop_front();
+ }
+ void clear() {
+ front_pos = 0;
+ back_pos = 0;
+ }
+ class iterator {
+ private:
+ RingBuffer<VALUE_TYPE, CAPACITY> *buf;
+ size_type ix;
+ iterator(RingBuffer<VALUE_TYPE, CAPACITY> *bb, size_type ii)
+ : buf(bb), ix(ii) {}
+ friend class RingBuffer<VALUE_TYPE, CAPACITY>;
+ public:
+ iterator(const iterator &it) : buf(it.buf), ix(it.ix) {}
+ reference operator*() const { return buf->elems[ix]; }
+ pointer operator->() const { return &buf->elems[ix]; }
+ iterator &operator++() {
+ ix++;
+ if (ix == CAPACITY+1 && buf->back_pos != CAPACITY+1)
+ ix = 0;
+ return *this;
+ }
+ iterator operator++(int) {
+ iterator tmp = *this;
+ ++*this;
+ return tmp;
+ }
+ iterator &operator--() {
+ if(ix == 0)
+ ix = CAPACITY+1;
+ ix--;
+ return *this;
+ }
+ iterator operator--(int) {
+ iterator tmp = *this;
+ --*this;
+ return tmp;
+ }
+ bool operator==(const iterator &it) {
+ return buf == it.buf && ix == it.ix;
+ }
+ bool operator!=(const iterator &it) { return !(*this == it); }
+ };
+ iterator begin() { return iterator(this, front_pos); }
+ iterator end() { return iterator(this, back_pos); }
+};
+
+#endif