Client can now connect to servers interactively

This commit is contained in:
2024-02-22 17:30:24 -05:00
parent e68d63915d
commit 7f7bd5a781
8 changed files with 129 additions and 29 deletions

View File

@@ -22,10 +22,13 @@ public:
Gui gui_;
public:
void set_initial_state() {
void set_initial_state_connect(const eng::string &hostspec) {
// Create the world model.
world_.reset(new World(WORLD_TYPE_PREDICTIVE));
// Create the communication channel.
channel_ = new_outgoing_channel(hostspec);
// This is a temporary actor that will be used only until the server sends
// us the first difference transmission. We do this only to establish
// the invariant that there's always an actor. When the first difference
@@ -38,6 +41,34 @@ public:
// Export stuff to the graphics engine.
set_visible_world_and_actor(world_.get(), actor_id_);
// Reset the print channeler
print_channeler_.reset();
}
void set_initial_state_standalone(std::string_view srcpk) {
// Create the world model.
world_.reset(new World(WORLD_TYPE_MASTER));
// Update the source code of the master model.
world_->update_source(srcpk);
// Make sure the channel is empty.
channel_.reset();
// Create the standalone actor.
actor_id_ = world_->create_login_actor();
// TODO: initialize the standalone actor.
// Clear the unack command queue.
unack_.clear();
// Export stuff to the graphics engine.
set_visible_world_and_actor(world_.get(), actor_id_);
// Reset the print channeler
print_channeler_.reset();
}
@@ -54,30 +85,31 @@ public:
}
void world_to_asynchronous() {
if (world_->snapshot_empty()) {
world_->snapshot();
for (const Invocation &inv : unack_) {
world_->invoke(inv);
if (!world_->is_authoritative()) {
if (world_->snapshot_empty()) {
world_->snapshot();
for (const Invocation &inv : unack_) {
world_->invoke(inv);
}
}
}
}
void abandon_server() {
stdostream() << "Abandoning server." << std::endl;
// When we abandon the server, we leave the world model
// hanging around to preserve the invariant that there
// is always a world model. Then, we trigger a rescan
// of the lua source. When the lua source shows up, then
// we will create a standalone model to replace the client
// model.
// Put the world model back into a known-good state.
set_initial_state();
// Disconnect from the server.
channel_.reset();
rescan_lua_source();
}
virtual void event_init(std::string_view srcpk, int argc, char *argv[]) {
// Put the world into the starting state.
set_initial_state();
// Establish a connection to the server.
channel_ = new_outgoing_channel("nocert:localhost:8085");
set_initial_state_standalone(srcpk);
// Set the console prompt
set_console_prompt(console_.get_prompt());
@@ -85,15 +117,24 @@ public:
void send_invocation(const Invocation &inv) {
if (channel_ == nullptr) {
stdostream() << "Cannot invoke any actions, not connected." << std::endl;
return;
if ((!world_->is_authoritative()) && (inv.kind() == Invocation::KIND_LUA_SOURCE)) {
// We have a client model, but no client connection. That means we're
// in the process of shutting down a client model. The client model
// is supposed to linger until the lua source is reread. Once we have
// the lua source, we're supposed to throw out the client model and
// create a standalone model.
set_initial_state_standalone(inv.datapack());
} else {
world_->invoke(inv);
}
} else {
world_to_asynchronous();
world_->invoke(inv);
unack_.push_back(inv);
StreamBuffer *sb = channel_->out();
sb->write_uint8(util::MSG_INVOKE);
inv.serialize(sb);
}
world_to_asynchronous();
world_->invoke(inv);
unack_.push_back(inv);
StreamBuffer *sb = channel_->out();
sb->write_uint8(util::MSG_INVOKE);
inv.serialize(sb);
}
void do_luainvoke_command(const StringVec &words) {
@@ -149,6 +190,10 @@ public:
stop_driver();
}
void do_connect_command(const util::StringVec &words) {
set_initial_state_connect(util::ss("nocert:", words[1], ":8085"));
}
void do_command(const util::StringVec &words) {
if (words.empty()) return;
else if (words[0] == "luainvoke") do_luainvoke_command(words);
@@ -161,6 +206,7 @@ public:
else if (words[0] == "cpl") do_cpl_command(words);
else if (words[0] == "work") do_work_command(words);
else if (words[0] == "quit") do_quit_command(words);
else if (words[0] == "connect") do_connect_command(words);
else {
stdostream() << "Unsupported command: " << words[0] << std::endl;
}
@@ -256,9 +302,7 @@ public:
// Channel print statements.
if (print_channeler_.channel(world_->get_printbuffer(actor_id_), stdostream())) {
if (channel_ != nullptr) {
send_invocation(print_channeler_.invocation(actor_id_));
}
send_invocation(print_channeler_.invocation(actor_id_));
}
}
};

View File

@@ -41,6 +41,8 @@ public:
// Create an actor for administrative commands.
admin_id_ = master_->create_login_actor();
// TODO: initialize the admin actor.
// Print out admin ID for debugging purposes.
stdostream() << "Admin actor id = " << admin_id_ << std::endl;
@@ -196,8 +198,11 @@ public:
if (chan->port() == 8085) {
Client *client = new Client;
client->actor_id_ = master_->create_login_actor();
// TODO: initialize the login actor on the master.
client->channel_ = std::move(chan);
client->sync_.reset(new World(WORLD_TYPE_PREDICTIVE));
// This login actor is never used, it is just to preserve the invariant that
// the client model and the server synchronous model are identical.
client->sync_->create_login_actor();
clients_.emplace_back(client);
stdostream() << "New client: actor id=" << client->actor_id_ << std::endl;

View File

@@ -102,6 +102,12 @@ void LuaConsole::simplify(const StringVec &words) {
if (words.size() != 1) {
synerr("/aborthttp takes no arguments");
}
} else if (words[0] == "connect") {
if ((words.size() == 2)&&(sv::valid_hostname(words[1]))) {
// OK
} else {
synerr("/connect [hostname]");
}
} else {
synerr("unrecognized command");
}

View File

@@ -16,6 +16,9 @@ struct PrintBufferCore : public eng::opnew {
// Line number of the first unchecked line in the buffer. All line
// numbers including this one and beyond are unchecked.
// "Unchecked" lines are lines in a non-authoritative model
// that haven't been verified through difference transmission.
int first_unchecked_;
// Constructor.

View File

@@ -104,6 +104,8 @@ public:
int last_line() const;
// Get the current first unchecked line.
// "Unchecked" lines are lines in a non-authoritative model
// that haven't been verified through difference transmission.
// Guaranteed to be between first_line and last_line inclusive.
int first_unchecked() const;

View File

@@ -55,6 +55,30 @@ bool valid_double(string_view value) {
return true;
}
bool valid_hostname(string_view value) {
bool start_word = true;
bool last_dash = false;
for (char c : value) {
if (c == '.') {
if (start_word) return false;
if (last_dash) return false;
start_word = true;
last_dash = false;
} else if (c == '-') {
if (start_word) return false;
start_word = false;
last_dash = true;
} else if (ascii_isalnum(c)) {
start_word = false;
last_dash = false;
} else {
return false;
}
}
if (start_word || last_dash) return false;
return true;
}
int64_t to_int64(string_view value, int64_t errval) {
int64_t result;
const char *p = value.data();
@@ -810,7 +834,18 @@ static std::string_view read_number_x(const char *p, bool plus, bool minus, bool
}
LuaDefine(unittests_util, "", "some unit tests") {
// test str_to_int64, str_to_double
// Test valid_hostname
LuaAssert(L, sv::valid_hostname("foo123"));
LuaAssert(L, !sv::valid_hostname("foo%123"));
LuaAssert(L, sv::valid_hostname("foo-bar"));
LuaAssert(L, !sv::valid_hostname("-foo"));
LuaAssert(L, !sv::valid_hostname("foo-"));
LuaAssert(L, sv::valid_hostname("foo.bar.baz"));
LuaAssert(L, sv::valid_hostname("foo-bar.baz"));
LuaAssert(L, !sv::valid_hostname("foo-bar-.baz"));
LuaAssert(L, !sv::valid_hostname("foo.-bar-baz"));
// test str_to_int64, str_to_double
LuaAssert(L, sv::to_int64("123") == 123);
LuaAssert(L, sv::to_int64("123.4") == INT64_MAX);
LuaAssert(L, sv::to_int64("12ab") == INT64_MAX);

View File

@@ -62,6 +62,9 @@ bool valid_double(string_view v);
bool valid_int64(string_view v);
bool valid_hex64(string_view v);
// Check if a hostname is a valid DNS (ascii) hostname.
bool valid_hostname(string_view v);
// Convert strings to numbers. Returns errval on failure.
//
// The integer parser accepts a sequence of digits,

View File

@@ -1083,9 +1083,11 @@ void World::close_lthread_state() {
// send the output to std::cerr.
if (lthread_prints_ != nullptr) {
const eng::string &output = lthread_prints_->str();
Tangible *actor = tangible_get(lthread_actor_id_);
if (actor != nullptr) {
actor->print_buffer_.add_string(output, is_authoritative());
if (output.size() > 0) {
Tangible *actor = tangible_get(lthread_actor_id_);
if (actor != nullptr) {
actor->print_buffer_.add_string(output, is_authoritative());
}
}
}
// Now clean up everything.