Implemented PrintBuffer class. (Whew).
This commit is contained in:
@@ -31,6 +31,7 @@ CPP_FILES=\
|
|||||||
cpp/lpxserver.cpp\
|
cpp/lpxserver.cpp\
|
||||||
cpp/lpxclient.cpp\
|
cpp/lpxclient.cpp\
|
||||||
cpp/drivertests.cpp\
|
cpp/drivertests.cpp\
|
||||||
|
cpp/printbuffer.cpp\
|
||||||
cpp/main.cpp
|
cpp/main.cpp
|
||||||
|
|
||||||
OBJ_FILES=$(patsubst cpp/%.cpp,obj/%.o,$(CPP_FILES))
|
OBJ_FILES=$(patsubst cpp/%.cpp,obj/%.o,$(CPP_FILES))
|
||||||
|
|||||||
156
luprex/core/cpp/printbuffer.cpp
Normal file
156
luprex/core/cpp/printbuffer.cpp
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#include "printbuffer.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
PrintBuffer::PrintBuffer(util::WorldType wt) {
|
||||||
|
world_type_ = wt;
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int first_line_len(const char *text, int len) {
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
if (text[i] == '\n') return i;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintBuffer::add_line(const char *text, int len) {
|
||||||
|
lines_.emplace_back(text, len);
|
||||||
|
if ((world_type_ == util::WORLD_TYPE_MASTER)||(world_type_ == util::WORLD_TYPE_STANDALONE)) {
|
||||||
|
first_unchecked_ = first_line_ + int(lines_.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintBuffer::add_string(const char *text, int len) {
|
||||||
|
while (true) {
|
||||||
|
int fll = first_line_len(text, len);
|
||||||
|
if (fll == len) {
|
||||||
|
if (len > 0) {
|
||||||
|
add_line(text, len);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
add_line(text, fll);
|
||||||
|
text += (fll + 1);
|
||||||
|
len -= (fll + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintBuffer::add_string(const std::string &s) {
|
||||||
|
add_string(s.c_str(), s.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintBuffer::discard_upto(int n) {
|
||||||
|
while ((!lines_.empty()) && (first_line_ < n)) {
|
||||||
|
lines_.pop_front();
|
||||||
|
first_line_ += 1;
|
||||||
|
}
|
||||||
|
if (first_line_ < n) {
|
||||||
|
first_line_ = n;
|
||||||
|
}
|
||||||
|
if (first_unchecked_ < first_line_) {
|
||||||
|
first_unchecked_ = first_line_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintBuffer::clear() {
|
||||||
|
first_line_ = 0;
|
||||||
|
first_unchecked_ = 0;
|
||||||
|
lines_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PrintBuffer::debug_string() const {
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << first_line_ << "," << first_unchecked_ << ":";
|
||||||
|
for (int i = 0; i < int(lines_.size()); i++) {
|
||||||
|
oss << lines_[i] << ";";
|
||||||
|
}
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PrintBuffer::diff(const PrintBuffer &auth, StreamBuffer *sb) const {
|
||||||
|
// Currently, the implementation is simple. The synchronous model discards
|
||||||
|
// all prediction lines from its buffer, then the server retransmits all
|
||||||
|
// those lines. So this barely counts as difference transmission - it's
|
||||||
|
// just transmission, regardless of what was in the synchronous model. I
|
||||||
|
// think that's okay for the text console.
|
||||||
|
// Ask the client to discard anything that precedes auth_first.
|
||||||
|
sb->write_int32(auth.first_line_);
|
||||||
|
sb->write_int32(auth.last_line());
|
||||||
|
for (int i = first_unchecked_; i < auth.last_line(); i++) {
|
||||||
|
std::string line;
|
||||||
|
if (i >= auth.first_line_) line = auth.nth(i);
|
||||||
|
sb->write_string(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintBuffer::patch(StreamBuffer *sb) {
|
||||||
|
int auth_first = sb->read_int32();
|
||||||
|
int auth_last = sb->read_int32();
|
||||||
|
lines_.resize(first_unchecked_ - first_line_);
|
||||||
|
for (int i = first_unchecked_; i < auth_last; i++) {
|
||||||
|
lines_.emplace_back(sb->read_string());
|
||||||
|
}
|
||||||
|
first_unchecked_ = first_line_ + lines_.size();
|
||||||
|
discard_upto(auth_first);
|
||||||
|
}
|
||||||
|
|
||||||
|
LuaDefine(unittests_printbuffer, "c") {
|
||||||
|
PrintBuffer pbm(util::WORLD_TYPE_MASTER);
|
||||||
|
PrintBuffer pbs(util::WORLD_TYPE_S_SYNC);
|
||||||
|
StreamBuffer sb;
|
||||||
|
|
||||||
|
LuaAssertStrEq(L, pbm.debug_string(), "0,0:");
|
||||||
|
pbm.add_string("foo\nbar\nbaz\n");
|
||||||
|
LuaAssertStrEq(L, pbm.debug_string(), "0,3:foo;bar;baz;");
|
||||||
|
pbm.add_string("a\nb\nc");
|
||||||
|
LuaAssertStrEq(L, pbm.debug_string(), "0,6:foo;bar;baz;a;b;c;");
|
||||||
|
pbm.discard_upto(2);
|
||||||
|
LuaAssertStrEq(L, pbm.debug_string(), "2,6:baz;a;b;c;");
|
||||||
|
pbm.discard_upto(8);
|
||||||
|
LuaAssertStrEq(L, pbm.debug_string(), "8,8:");
|
||||||
|
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "0,0:");
|
||||||
|
pbs.add_string("foo\nbar\nbaz\n");
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "0,0:foo;bar;baz;");
|
||||||
|
pbs.add_string("a\nb\nc");
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "0,0:foo;bar;baz;a;b;c;");
|
||||||
|
pbs.discard_upto(2);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "2,2:baz;a;b;c;");
|
||||||
|
pbs.discard_upto(8);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "8,8:");
|
||||||
|
|
||||||
|
pbm.clear();
|
||||||
|
pbs.clear();
|
||||||
|
sb.clear();
|
||||||
|
pbm.add_string("foo\nbar\n");
|
||||||
|
pbs.diff(pbm, &sb);
|
||||||
|
pbs.patch(&sb);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "0,2:foo;bar;");
|
||||||
|
pbm.clear();
|
||||||
|
pbm.add_string("foo\nyow\nding\ndong\n");
|
||||||
|
pbs.diff(pbm, &sb);
|
||||||
|
pbs.patch(&sb);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "0,4:foo;bar;ding;dong;");
|
||||||
|
pbs.discard_upto(2);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
|
||||||
|
pbs.diff(pbm, &sb);
|
||||||
|
pbs.patch(&sb);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
|
||||||
|
pbs.add_string("boy\nhowdy\n");
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;boy;howdy;");
|
||||||
|
pbs.diff(pbm, &sb);
|
||||||
|
pbs.patch(&sb);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
|
||||||
|
pbs.add_string("boy\nhowdy\nyeah\nbaby\nget\ndown\n");
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;boy;howdy;yeah;baby;get;down;");
|
||||||
|
pbs.discard_upto(5);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "5,5:howdy;yeah;baby;get;down;");
|
||||||
|
pbs.diff(pbm, &sb);
|
||||||
|
pbs.patch(&sb);
|
||||||
|
LuaAssertStrEq(L, pbs.debug_string(), "5,5:");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
132
luprex/core/cpp/printbuffer.hpp
Normal file
132
luprex/core/cpp/printbuffer.hpp
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// PRINTBUFFER
|
||||||
|
//
|
||||||
|
// Lua has a 'print' statement - in client-server mode, where does the output of
|
||||||
|
// the 'print' statement go? We need to be able to handle print-statements on
|
||||||
|
// the server (and forward them to the client), and we need for predictive
|
||||||
|
// prints to be handled gracefully.
|
||||||
|
//
|
||||||
|
// Class PrintBuffer is a buffer for storing the output of the print statement.
|
||||||
|
// It is part of the actor tangible. When a lua thread calls 'print', the print
|
||||||
|
// goes into stringstream lthread_console. When the thread stops or yields, the
|
||||||
|
// contents of lthread_console are converted to lines and transferred to the
|
||||||
|
// PrintBuffer of the actor. From there, it gets difference transmitted, and the
|
||||||
|
// client can probe it.
|
||||||
|
//
|
||||||
|
// Suppose, for example, that the player logs in and invokes a plan that prints
|
||||||
|
// six lines from a Robert Frost poem. That plan is executed by both the master
|
||||||
|
// model and the synchronous model. When the thread finishes, the PrintBuffer
|
||||||
|
// in the actor in the master model will contain this:
|
||||||
|
//
|
||||||
|
// * Block 0: Whose woods these are I think I know. [authoritative]
|
||||||
|
// * Block 1: His house is in the village though; [authoritative]
|
||||||
|
// * Block 2: He will not see me stopping here [authoritative]
|
||||||
|
// * Block 3: To watch his woods fill up with snow. [authoritative]
|
||||||
|
// * Block 4: My little horse must think it queer [authoritative]
|
||||||
|
// * Block 5: To stop without a farmhouse near. [authoritative]
|
||||||
|
//
|
||||||
|
// Note that the buffer stores line numbers, which start from zero the moment
|
||||||
|
// the player logs in. In the master model, all lines are always authoritative
|
||||||
|
// because everything in the master model is authoritative. In the synchronous
|
||||||
|
// model, the PrintBuffer is likely to contain the same strings, but the lines
|
||||||
|
// will all be marked as [prediction] instead of [authoritative].
|
||||||
|
//
|
||||||
|
// When the server difference transmits, the difference transmission will fix up
|
||||||
|
// any mistakes in the PrintBuffer in the synchronous model. In the process, it
|
||||||
|
// will also fix up any [prediction] changing it to [authoritative].
|
||||||
|
//
|
||||||
|
// Periodically, the oldest lines in the buffer will get discarded. More on
|
||||||
|
// this later. When lines get discarded, the line numbers don't change. For
|
||||||
|
// example, if we were to discard three lines from the buffer above, this is
|
||||||
|
// what would remain:
|
||||||
|
//
|
||||||
|
// * Line 3: To watch his woods fill up with snow. [authoritative]
|
||||||
|
// * Line 4: My little horse must think it queer [authoritative]
|
||||||
|
// * Line 5: To stop without a farmhouse near. [authoritative]
|
||||||
|
//
|
||||||
|
// The client will periodically probe the PrintBuffer. Suppose it sees all six
|
||||||
|
// lines of the Robert Frost poem. The next time it probes the buffer, it will
|
||||||
|
// still see those same six lines. The client will continue to see those six
|
||||||
|
// lines until it takes explicit steps.
|
||||||
|
//
|
||||||
|
// Here's how we keep the buffer from growing forever. The client probes the
|
||||||
|
// PrintBuffer, and sees some authoritative lines. The client downloads and
|
||||||
|
// stores those lines locally. Once the lines are stored locally in the client,
|
||||||
|
// the client injects a command into the command stream: "delete from
|
||||||
|
// PrintBuffer up to line N" into the command stream. This will cause the
|
||||||
|
// engine to discard the lines that the client no longer needs.
|
||||||
|
//
|
||||||
|
// Note that when the client injects a "delete from PrintBuffer" into the
|
||||||
|
// command stream, that's an invoke-command that has to follow the same process
|
||||||
|
// as any other client invoke command. It can take time for it to get
|
||||||
|
// processed. Therefore, the client must be prepared that it might see some
|
||||||
|
// redundant lines for a little while.
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef PRINTBUFFER_HPP
|
||||||
|
#define PRINTBUFFER_HPP
|
||||||
|
|
||||||
|
#include "streambuffer.hpp"
|
||||||
|
#include "util.hpp"
|
||||||
|
#include <deque>
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class PrintBuffer {
|
||||||
|
private:
|
||||||
|
// The most recent lines printed.
|
||||||
|
std::deque<std::string> lines_;
|
||||||
|
|
||||||
|
// Line number of the first line in the buffer. From this, all other
|
||||||
|
// line numbers can be inferred.
|
||||||
|
int first_line_;
|
||||||
|
|
||||||
|
// Line number of the first unchecked line in the buffer. All line
|
||||||
|
// numbers including this one and beyond are unchecked.
|
||||||
|
int first_unchecked_;
|
||||||
|
|
||||||
|
// The world type of the enclosing model. This is used to determine
|
||||||
|
// whether add_string increments the n_checked counter or not.
|
||||||
|
util::WorldType world_type_;
|
||||||
|
|
||||||
|
// Add a line of text (internal). Line is expected to NOT contain
|
||||||
|
// a newline (or any other weird control characters).
|
||||||
|
void add_line(const char *line, int len);
|
||||||
|
|
||||||
|
// Return the line number beyond the end of the buffer.
|
||||||
|
int last_line() const { return first_line_ + int(lines_.size()); }
|
||||||
|
|
||||||
|
// Get the specified line number.
|
||||||
|
const std::string &nth(int n) const { return lines_[n - first_line_]; }
|
||||||
|
|
||||||
|
|
||||||
|
public:
|
||||||
|
PrintBuffer(util::WorldType wt);
|
||||||
|
|
||||||
|
// Add a string. If the string doesn't end in a newline, a newline
|
||||||
|
// is added. The string is broken into lines, and the lines are added
|
||||||
|
// to the PrintBuffer.
|
||||||
|
void add_string(const char *text, int len);
|
||||||
|
void add_string(const std::string &s);
|
||||||
|
|
||||||
|
// Discard lines up to but not including line N.
|
||||||
|
void discard_upto(int n);
|
||||||
|
|
||||||
|
// Difference transmission
|
||||||
|
void diff(const PrintBuffer &auth, StreamBuffer *sb) const;
|
||||||
|
void patch(StreamBuffer *sb);
|
||||||
|
|
||||||
|
// Clear the buffer (for unit testing)
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
// Print the entire contents of the buffer to a string (for unit testing).
|
||||||
|
std::string debug_string() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
using UniquePrintBuffer = std::unique_ptr<PrintBuffer>;
|
||||||
|
|
||||||
|
#endif // PRINTBUFFER_HPP
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "animqueue.hpp"
|
#include "animqueue.hpp"
|
||||||
#include "invocation.hpp"
|
#include "invocation.hpp"
|
||||||
#include "streambuffer.hpp"
|
#include "streambuffer.hpp"
|
||||||
|
#include "printbuffer.hpp"
|
||||||
#include "sched.hpp"
|
#include "sched.hpp"
|
||||||
#include "source.hpp"
|
#include "source.hpp"
|
||||||
#include "gui.hpp"
|
#include "gui.hpp"
|
||||||
@@ -62,6 +63,13 @@ public:
|
|||||||
//
|
//
|
||||||
IdPlayerPool id_player_pool_;
|
IdPlayerPool id_player_pool_;
|
||||||
|
|
||||||
|
// Print Buffer
|
||||||
|
//
|
||||||
|
// In non-actors, this is a null pointer. Stores the console
|
||||||
|
// output for this actor until it can be probed by the client.
|
||||||
|
//
|
||||||
|
UniquePrintBuffer print_buffer_;
|
||||||
|
|
||||||
// constructor.
|
// constructor.
|
||||||
//
|
//
|
||||||
Tangible(World *w, int64_t id);
|
Tangible(World *w, int64_t id);
|
||||||
|
|||||||
Reference in New Issue
Block a user