From de7fde4e17cf6c5501494932767e385ac71c31b9 Mon Sep 17 00:00:00 2001 From: Zefram Date: Sat, 9 Aug 2014 10:53:57 +0100 Subject: [PATCH] Preserve node predictions until server catches up Apply implicit sequence numbers on actions sent by clients, and use them for the server to indicate, in a node status update, how up to date it is in processing the client's actions. Rather than apply all node updates blindly, the client now checks whether it has relevant predictions that are more recent than what the server has used to produce the update, and keep those predictions applied until the server has caught up. This avoids lag-induced node glitches. --- src/client.cpp | 97 +++++++++++++++++++++++++++++++++++ src/client.h | 47 +++++++++++++++++ src/clientiface.h | 3 ++ src/clientserver.h | 12 +++++ src/game.cpp | 10 +++- src/server.cpp | 57 ++++++++++++++++----- src/server.h | 2 +- src/util/ringbuffer.h | 115 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 src/util/ringbuffer.h diff --git a/src/client.cpp b/src/client.cpp index 601561f7..eb7f5403 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -258,6 +258,7 @@ Client::Client( 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), @@ -1047,6 +1048,8 @@ void Client::ProcessData(u8 *data, u32 datasize, u16 sender_peer_id) m_con.Send(PEER_ID_SERVER, 1, reply, true); m_state = LC_Init; + m_last_action_seq = 0; + m_node_predictions.clear(); return; } @@ -1091,6 +1094,19 @@ void Client::ProcessData(u8 *data, u32 datasize, u16 sender_peer_id) p.X = readS16(&data[2]); p.Y = readS16(&data[4]); p.Z = readS16(&data[6]); + if(datasize >= 10) + { + for(RingBuffer::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) @@ -1112,6 +1128,20 @@ void Client::ProcessData(u8 *data, u32 datasize, u16 sender_peer_id) remove_metadata = false; } + if(datasize >= index+3) + { + for(RingBuffer::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) @@ -1156,6 +1186,21 @@ void Client::ProcessData(u8 *data, u32 datasize, u16 sender_peer_id) sector->insertBlock(block); } + try + { + v3s16 brpos = block->getPosRelative(); + for(RingBuffer::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. */ @@ -1966,6 +2011,30 @@ void Client::Send(u16 channelnum, SharedBuffer data, bool reliable) 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::iterator +Client::firstPendingPrediction(u16 last_action_considered) +{ + RingBuffer::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){ @@ -2002,6 +2071,8 @@ void Client::interact(u8 action, const PointedThing& pointed) // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::sendNodemetaFields(v3s16 p, const std::string &formname, @@ -2028,6 +2099,8 @@ void Client::sendNodemetaFields(v3s16 p, const std::string &formname, SharedBuffer data((u8*)s.c_str(), s.size()); // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::sendInventoryFields(const std::string &formname, @@ -2053,6 +2126,8 @@ void Client::sendInventoryFields(const std::string &formname, SharedBuffer data((u8*)s.c_str(), s.size()); // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::sendInventoryAction(InventoryAction *a) @@ -2071,6 +2146,8 @@ void Client::sendInventoryAction(InventoryAction *a) SharedBuffer data((u8*)s.c_str(), s.size()); // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::sendChatMessage(const std::wstring &message) @@ -2103,6 +2180,8 @@ void Client::sendChatMessage(const std::wstring &message) SharedBuffer data((u8*)s.c_str(), s.size()); // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::sendChangePassword(const std::wstring &oldpassword, @@ -2139,6 +2218,8 @@ void Client::sendChangePassword(const std::wstring &oldpassword, SharedBuffer data((u8*)s.c_str(), s.size()); // Send as reliable Send(0, data, true); + + incrementActionSeq(); } @@ -2155,6 +2236,8 @@ void Client::sendDamage(u8 damage) SharedBuffer data((u8*)s.c_str(), s.size()); // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::sendBreath(u16 breath) @@ -2169,6 +2252,8 @@ void Client::sendBreath(u16 breath) SharedBuffer data((u8*)s.c_str(), s.size()); // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::sendRespawn() @@ -2183,6 +2268,8 @@ void Client::sendRespawn() SharedBuffer data((u8*)s.c_str(), s.size()); // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::sendReady() @@ -2286,6 +2373,8 @@ void Client::sendPlayerItem(u16 item) // Send as reliable Send(0, data, true); + + incrementActionSeq(); } void Client::removeNode(v3s16 p) @@ -2332,6 +2421,14 @@ void Client::addNode(v3s16 p, MapNode n, bool remove_metadata) 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) { diff --git a/src/client.h b/src/client.h index 51ce5b8f..f8a38f56 100644 --- a/src/client.h +++ b/src/client.h @@ -34,6 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "localplayer.h" #include "hud.h" #include "particles.h" +#include "util/ringbuffer.h" struct MeshMakeData; class MapBlockMesh; @@ -292,6 +293,45 @@ private: std::map 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: @@ -363,6 +403,7 @@ 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); @@ -473,6 +514,10 @@ private: void sendPlayerPos(); // Send the item number 'item' as player item to the server void sendPlayerItem(u16 item); + + void incrementActionSeq(); + RingBuffer::iterator + firstPendingPrediction(u16 last_action_considered); float m_packetcounter_timer; float m_connection_reinit_timer; @@ -517,6 +562,8 @@ private: bool m_itemdef_received; bool m_nodedef_received; ClientMediaDownloader *m_media_downloader; + u16 m_last_action_seq; + RingBuffer m_node_predictions; // time_of_day speed approximation for old protocol bool m_time_of_day_set; diff --git a/src/clientiface.h b/src/clientiface.h index 5452ccdd..8dfde1c3 100644 --- a/src/clientiface.h +++ b/src/clientiface.h @@ -199,11 +199,14 @@ public: 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), diff --git a/src/clientserver.h b/src/clientserver.h index f12384b1..15785b3d 100644 --- a/src/clientserver.h +++ b/src/clientserver.h @@ -146,14 +146,26 @@ enum ToClientCommand */ 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 /* diff --git a/src/game.cpp b/src/game.cpp index 76819314..d0573e38 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -832,7 +832,8 @@ public: }; 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(); @@ -916,6 +917,7 @@ bool nodePlacementPrediction(Client &client, // This triggers the required mesh update too client.addNode(p, n); + *predicted_pos_p = p; return true; } }catch(InvalidPositionException &e){ @@ -2917,6 +2919,7 @@ void the_game(bool &kill, bool random_input, InputHandler *input, client.setCrack(-1, v3s16(0,0,0)); MapNode wasnode = map.getNode(nodepos); client.removeNode(nodepos); + client.notePredictedNode(nodepos); if (g_settings->getBool("enable_particles")) { @@ -2990,13 +2993,16 @@ void the_game(bool &kill, bool random_input, InputHandler *input, // 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; diff --git a/src/server.cpp b/src/server.cpp index 40857f84..e0e8b947 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -1617,6 +1617,10 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) verbosestream<<"Server: Got TOSERVER_INIT2 from " <last_action_seq = 0; + m_clients.event(peer_id, CSE_GotInit2); u16 protocol_version = m_clients.getProtocolVersion(peer_id); @@ -1672,7 +1676,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 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!"); @@ -1916,6 +1920,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } 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="<last_action_seq++; /* u16 command u16 length @@ -2153,6 +2159,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } 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); @@ -2174,6 +2181,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } 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); @@ -2182,6 +2190,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } else if(command == TOSERVER_PASSWORD) { + getClient(peer_id)->last_action_seq++; /* [0] u16 TOSERVER_PASSWORD [2] u8[28] old password @@ -2246,6 +2255,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } else if(command == TOSERVER_PLAYERITEM) { + getClient(peer_id)->last_action_seq++; if (datasize < 2+2) return; @@ -2254,6 +2264,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } else if(command == TOSERVER_RESPAWN) { + getClient(peer_id)->last_action_seq++; if(player->hp != 0 || !g_settings->getBool("enable_damage")) return; @@ -2267,6 +2278,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } 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); @@ -2654,6 +2666,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } 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); @@ -2686,6 +2699,7 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) } 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); @@ -3665,14 +3679,6 @@ void Server::sendRemoveNode(v3s16 p, u16 ignore_id, float maxd = far_d_nodes*BS; v3f p_f = intToFloat(p, BS); - // Create packet - u32 replysize = 8; - SharedBuffer reply(replysize); - writeU16(&reply[0], TOCLIENT_REMOVENODE); - writeS16(&reply[2], p.X); - writeS16(&reply[4], p.Y); - writeS16(&reply[6], p.Z); - std::list clients = m_clients.getClientIDs(); for(std::list::iterator i = clients.begin(); @@ -3694,8 +3700,26 @@ void Server::sendRemoveNode(v3s16 p, u16 ignore_id, } } + SharedBuffer 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 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); } } @@ -3734,6 +3758,8 @@ void Server::sendAddNode(v3s16 p, MapNode n, u16 ignore_id, { // Create packet u32 replysize = 9 + MapNode::serializedLength(client->serialization_version); + if (client->net_proto_version >= 23) + replysize += 2; reply = SharedBuffer(replysize); writeU16(&reply[0], TOCLIENT_ADDNODE); writeS16(&reply[2], p.X); @@ -3742,6 +3768,8 @@ void Server::sendAddNode(v3s16 p, MapNode n, u16 ignore_id, 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) { @@ -3773,7 +3801,8 @@ void Server::setBlockNotSent(v3s16 p) 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); @@ -3811,12 +3840,16 @@ void Server::SendBlockNoLock(u16 peer_id, MapBlock *block, u8 ver, u16 net_proto SharedBuffer blockdata((u8*)s.c_str(), s.size()); u32 replysize = 8 + blockdata.getSize(); + if (net_proto_version >= 23) + replysize += 2; SharedBuffer 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 ("<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++; diff --git a/src/server.h b/src/server.h index cb0bacec..71aea992 100644 --- a/src/server.h +++ b/src/server.h @@ -392,7 +392,7 @@ private: 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); diff --git a/src/util/ringbuffer.h b/src/util/ringbuffer.h new file mode 100644 index 00000000..13077adb --- /dev/null +++ b/src/util/ringbuffer.h @@ -0,0 +1,115 @@ +#ifndef UTIL_RINGBUFFER_HEADER +#define UTIL_RINGBUFFER_HEADER + +#include + +// 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 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 *buf; + size_type ix; + iterator(RingBuffer *bb, size_type ii) + : buf(bb), ix(ii) {} + friend class RingBuffer; + 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 -- 2.30.2