diff --git a/luprex/core/Makefile b/luprex/core/Makefile index 1b571578..f70dba17 100644 --- a/luprex/core/Makefile +++ b/luprex/core/Makefile @@ -31,6 +31,7 @@ CPP_FILES=\ cpp/lpxserver.cpp\ cpp/lpxclient.cpp\ cpp/drivertests.cpp\ + cpp/printbuffer.cpp\ cpp/main.cpp OBJ_FILES=$(patsubst cpp/%.cpp,obj/%.o,$(CPP_FILES)) diff --git a/luprex/core/cpp/printbuffer.cpp b/luprex/core/cpp/printbuffer.cpp new file mode 100644 index 00000000..42404afe --- /dev/null +++ b/luprex/core/cpp/printbuffer.cpp @@ -0,0 +1,156 @@ +#include "printbuffer.hpp" +#include +#include + +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; +} diff --git a/luprex/core/cpp/printbuffer.hpp b/luprex/core/cpp/printbuffer.hpp new file mode 100644 index 00000000..f939df90 --- /dev/null +++ b/luprex/core/cpp/printbuffer.hpp @@ -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 +#include +#include + +class PrintBuffer { +private: + // The most recent lines printed. + std::deque 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; + +#endif // PRINTBUFFER_HPP + diff --git a/luprex/core/cpp/world.hpp b/luprex/core/cpp/world.hpp index 9eb4d9f7..de957214 100644 --- a/luprex/core/cpp/world.hpp +++ b/luprex/core/cpp/world.hpp @@ -8,6 +8,7 @@ #include "animqueue.hpp" #include "invocation.hpp" #include "streambuffer.hpp" +#include "printbuffer.hpp" #include "sched.hpp" #include "source.hpp" #include "gui.hpp" @@ -62,6 +63,13 @@ public: // 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. // Tangible(World *w, int64_t id);