516 lines
14 KiB
C++
516 lines
14 KiB
C++
// http://www.apache.org/licenses/LICENSE-2.0
|
|
// Copyright 2014 Perttu Ahola <celeron55@gmail.com>
|
|
#include "core/log.h"
|
|
#include "client/state.h"
|
|
#include "client/app.h"
|
|
#include "client/config.h"
|
|
#include "interface/tcpsocket.h"
|
|
#include "interface/packet_stream.h"
|
|
#include "interface/sha1.h"
|
|
#include "interface/fs.h"
|
|
#include "lua_bindings/replicate.h"
|
|
#include <c55/string_util.h>
|
|
#include <cereal/archives/portable_binary.hpp>
|
|
#include <cereal/types/string.hpp>
|
|
#include <Node.h>
|
|
#include <Scene.h>
|
|
#include <MemoryBuffer.h>
|
|
#include <SmoothedTransform.h>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <deque>
|
|
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
// Without this some of the network functions are not found on mingw
|
|
#ifndef _WIN32_WINNT
|
|
#define _WIN32_WINNT 0x0501
|
|
#endif
|
|
#include <windows.h>
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#ifdef _MSC_VER
|
|
#pragma comment(lib, "ws2_32.lib")
|
|
#endif
|
|
typedef int socklen_t;
|
|
#else
|
|
#include <sys/socket.h>
|
|
#endif
|
|
|
|
#define MODULE "__state"
|
|
namespace magic = Urho3D;
|
|
|
|
using magic::Node;
|
|
using magic::Component;
|
|
using magic::SmoothedTransform;
|
|
|
|
extern client::Config g_client_config;
|
|
|
|
namespace client {
|
|
|
|
struct CState: public State
|
|
{
|
|
sp_<interface::TCPSocket> m_socket;
|
|
std::deque<char> m_socket_buffer;
|
|
interface::PacketStream m_packet_stream;
|
|
sp_<app::App> m_app;
|
|
ss_ m_remote_cache_path;
|
|
ss_ m_tmp_path;
|
|
sm_<ss_, ss_> m_file_hashes; // name -> hash
|
|
set_<ss_> m_waiting_files; // name
|
|
bool m_tell_after_all_files_transferred_requested = false;
|
|
// Connecting is possible only once. After that has happened, the whole
|
|
// state has to be recreated for making a new connection.
|
|
// In actuality the whole client application has to be recreated because
|
|
// otherwise unwanted Lua state remains.
|
|
bool m_connected = false;
|
|
sm_<ss_, std::function<void(const ss_ &, const ss_ &)>> m_packet_handlers;
|
|
|
|
CState(sp_<app::App> app):
|
|
m_socket(interface::createTCPSocket()),
|
|
m_app(app),
|
|
m_remote_cache_path(g_client_config.get<ss_>("cache_path")+"/remote"),
|
|
m_tmp_path(g_client_config.get<ss_>("cache_path")+"/tmp")
|
|
{
|
|
// Create directory for cached files
|
|
interface::fs::create_directories(m_remote_cache_path);
|
|
interface::fs::create_directories(m_tmp_path);
|
|
|
|
setup_packet_handlers();
|
|
}
|
|
|
|
void update()
|
|
{
|
|
if(m_socket->wait_data(0)){
|
|
read_socket();
|
|
handle_socket_buffer();
|
|
}
|
|
}
|
|
|
|
bool connect_host_port(const ss_ &address, const ss_ &port, ss_ *error)
|
|
{
|
|
if(m_connected){
|
|
log_i(MODULE, "client::State: Cannot re-use state for new connection");
|
|
if(error)
|
|
*error = "Cannot re-use state for new connection";
|
|
return false;
|
|
}
|
|
|
|
bool ok = m_socket->connect_fd(address, port);
|
|
if(ok){
|
|
log_i(MODULE, "client::State: Connect succeeded (%s:%s)",
|
|
cs(address), cs(port));
|
|
m_connected = true;
|
|
} else {
|
|
log_i(MODULE, "client::State: Connect failed (%s:%s)",
|
|
cs(address), cs(port));
|
|
if(error)
|
|
*error = "Connect failed";
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
bool connect(const ss_ &address, ss_ *error)
|
|
{
|
|
if(address.empty()){
|
|
if(error)
|
|
*error = "Cannot connect to empty address";
|
|
return false;
|
|
}
|
|
ss_ host;
|
|
ss_ port;
|
|
c55::Strfnd f(address);
|
|
if(address[0] == '['){
|
|
f.next("[");
|
|
host = f.next("]");
|
|
f.next(":");
|
|
port = f.next("");
|
|
} else {
|
|
host = f.next(":");
|
|
port = f.next("");
|
|
}
|
|
|
|
if(host == ""){
|
|
if(error)
|
|
*error = "Cannot connect to empty host";
|
|
return false;
|
|
}
|
|
if(port == ""){
|
|
port = "20000";
|
|
}
|
|
return connect_host_port(host, port, error);
|
|
}
|
|
|
|
void send_packet(const ss_ &name, const ss_ &data)
|
|
{
|
|
log_v(MODULE, "send_packet(): name=%s", cs(name));
|
|
m_packet_stream.output(name, data, [&](const ss_ &packet_data){
|
|
m_socket->send_fd(packet_data);
|
|
});
|
|
}
|
|
|
|
ss_ get_file_path(const ss_ &name, ss_ *dst_file_hash)
|
|
{
|
|
auto it = m_file_hashes.find(name);
|
|
if(it == m_file_hashes.end())
|
|
return "";
|
|
const ss_ &file_hash = it->second;
|
|
ss_ file_hash_hex = interface::sha1::hex(file_hash);
|
|
ss_ path = m_remote_cache_path+"/"+file_hash_hex;
|
|
if(dst_file_hash != nullptr)
|
|
*dst_file_hash = file_hash;
|
|
return path;
|
|
}
|
|
|
|
ss_ get_file_content(const ss_ &name)
|
|
{
|
|
ss_ file_hash;
|
|
ss_ path = get_file_path(name, &file_hash);
|
|
std::ifstream f(path);
|
|
if(!f.good())
|
|
throw Exception(ss_()+"Could not open file: "+path);
|
|
std::string file_content((std::istreambuf_iterator<char>(f)),
|
|
std::istreambuf_iterator<char>());
|
|
ss_ file_hash2 = interface::sha1::calculate(file_content);
|
|
if(file_hash != file_hash2){
|
|
log_e(MODULE, "Opened file differs in hash: \"%s\": "
|
|
"requested %s, actual %s", cs(name),
|
|
cs(interface::sha1::hex(file_hash)),
|
|
cs(interface::sha1::hex(file_hash2)));
|
|
throw Exception(ss_()+"Invalid file content: "+path);
|
|
}
|
|
return file_content;
|
|
}
|
|
|
|
void read_socket()
|
|
{
|
|
int fd = m_socket->fd();
|
|
char buf[100000];
|
|
ssize_t r = recv(fd, buf, 100000, 0);
|
|
if(r == -1)
|
|
throw Exception(ss_()+"Receive failed: "+strerror(errno));
|
|
if(r == 0){
|
|
log_w(MODULE, "Peer disconnected");
|
|
return;
|
|
}
|
|
log_d(MODULE, "Received %zu bytes", r);
|
|
m_socket_buffer.insert(m_socket_buffer.end(), buf, buf + r);
|
|
}
|
|
|
|
void handle_socket_buffer()
|
|
{
|
|
m_packet_stream.input(m_socket_buffer,
|
|
[&](const ss_ &name, const ss_ &data){
|
|
try {
|
|
handle_packet(name, data);
|
|
} catch(std::exception &e){
|
|
log_w(MODULE, "Exception on handling packet: %s", e.what());
|
|
}
|
|
});
|
|
}
|
|
|
|
void setup_packet_handlers();
|
|
|
|
void handle_packet(const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
auto it = m_packet_handlers.find(packet_name);
|
|
if(it == m_packet_handlers.end()){
|
|
// Pass forward
|
|
m_app->handle_packet(packet_name, data);
|
|
} else {
|
|
// Use handler
|
|
auto &handler = it->second;
|
|
handler(packet_name, data);
|
|
}
|
|
}
|
|
};
|
|
|
|
void CState::setup_packet_handlers()
|
|
{
|
|
m_packet_handlers["core:run_script"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
log_i(MODULE, "Asked to run script:\n----\n%s\n----", cs(data));
|
|
if(m_app)
|
|
m_app->run_script(data);
|
|
};
|
|
|
|
m_packet_handlers["core:announce_file"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
ss_ file_name;
|
|
ss_ file_hash;
|
|
std::istringstream is(data, std::ios::binary);
|
|
{
|
|
cereal::PortableBinaryInputArchive ar(is);
|
|
ar(file_name);
|
|
ar(file_hash);
|
|
}
|
|
m_file_hashes[file_name] = file_hash;
|
|
ss_ file_hash_hex = interface::sha1::hex(file_hash);
|
|
log_v(MODULE, "Server announces file: %s %s",
|
|
cs(file_hash_hex), cs(file_name));
|
|
// Check if we already have this file
|
|
ss_ path = m_remote_cache_path+"/"+file_hash_hex;
|
|
std::ifstream ifs(path, std::ios::binary);
|
|
bool cached_is_ok = false;
|
|
if(ifs.good()){
|
|
std::string content((std::istreambuf_iterator<char>(ifs)),
|
|
std::istreambuf_iterator<char>());
|
|
ss_ content_hash = interface::sha1::calculate(content);
|
|
if(content_hash == file_hash){
|
|
// We have it; no need to ask this file
|
|
log_i(MODULE, "%s %s: cached",
|
|
cs(file_hash_hex), cs(file_name));
|
|
cached_is_ok = true;
|
|
} else {
|
|
// Our copy is broken, re-request it
|
|
log_i(MODULE, "%s %s: Our copy is broken (has hash %s)",
|
|
cs(file_hash_hex), cs(file_name),
|
|
cs(interface::sha1::hex(content_hash)));
|
|
}
|
|
}
|
|
if(cached_is_ok){
|
|
// Let Lua resource wrapper know that this happened so it can update
|
|
// the copy made for Urho3D's resource cache
|
|
m_app->file_updated_in_cache(file_name, file_hash, path);
|
|
} else {
|
|
// We don't have it; request this file
|
|
log_i(MODULE, "%s %s: requesting",
|
|
cs(file_hash_hex), cs(file_name));
|
|
std::ostringstream os(std::ios::binary);
|
|
{
|
|
cereal::PortableBinaryOutputArchive ar(os);
|
|
ar(file_name);
|
|
ar(file_hash);
|
|
}
|
|
send_packet("core:request_file", os.str());
|
|
m_waiting_files.insert(file_name);
|
|
}
|
|
};
|
|
|
|
m_packet_handlers["core:tell_after_all_files_transferred"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
if(m_waiting_files.empty()){
|
|
send_packet("core:all_files_transferred", "");
|
|
} else {
|
|
m_tell_after_all_files_transferred_requested = true;
|
|
}
|
|
};
|
|
|
|
m_packet_handlers["core:file_content"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
ss_ file_name;
|
|
ss_ file_hash;
|
|
ss_ file_content;
|
|
std::istringstream is(data, std::ios::binary);
|
|
{
|
|
cereal::PortableBinaryInputArchive ar(is);
|
|
ar(file_name);
|
|
ar(file_hash);
|
|
ar(file_content);
|
|
}
|
|
if(m_waiting_files.count(file_name) == 0){
|
|
log_w(MODULE, "Received file was not requested: %s %s",
|
|
cs(interface::sha1::hex(file_hash)), cs(file_name));
|
|
return;
|
|
}
|
|
m_waiting_files.erase(file_name);
|
|
ss_ file_hash2 = interface::sha1::calculate(file_content);
|
|
if(file_hash != file_hash2){
|
|
log_w(MODULE, "Requested file differs in hash: \"%s\": "
|
|
"requested %s, actual %s", cs(file_name),
|
|
cs(interface::sha1::hex(file_hash)),
|
|
cs(interface::sha1::hex(file_hash2)));
|
|
return;
|
|
}
|
|
ss_ file_hash_hex = interface::sha1::hex(file_hash);
|
|
ss_ path = g_client_config.get<ss_>("cache_path")+"/remote/"+
|
|
file_hash_hex;
|
|
log_i(MODULE, "Saving %s to %s", cs(file_name), cs(path));
|
|
std::ofstream of(path, std::ios::binary);
|
|
of<<file_content;
|
|
if(m_tell_after_all_files_transferred_requested){
|
|
if(m_waiting_files.empty()){
|
|
send_packet("core:all_files_transferred", "");
|
|
m_tell_after_all_files_transferred_requested = false;
|
|
}
|
|
}
|
|
// Let Lua resource wrapper know that this happened so it can update
|
|
// the copy made for Urho3D's resource cache
|
|
m_app->file_updated_in_cache(file_name, file_hash, path);
|
|
};
|
|
|
|
m_packet_handlers["replicate:create_node"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
// For a reference implementation of this kind of network
|
|
// synchronization, see Urho3D's Network/Connection.cpp
|
|
|
|
magic::Scene *scene = m_app->get_scene();
|
|
magic::MemoryBuffer msg(data.c_str(), data.size());
|
|
uint node_id = msg.ReadNetID();
|
|
Node *node = scene->GetNode(node_id);
|
|
if(node && node_id == 1){
|
|
// This is the scene
|
|
} else if(node){
|
|
log_w(MODULE, "replicate:create_node: Node %i (old name=\"%s\")"
|
|
" already exists. This could be due to a node having been"
|
|
" accidentally created on the client side without mode=LOCAL."
|
|
" If a node seems to mysteriously disappear, this is the"
|
|
" reason.",
|
|
node_id, node->GetName().CString());
|
|
} else {
|
|
log_v(MODULE, "Creating node %i", node_id);
|
|
// Add to the root level; it may be moved as we receive the parent
|
|
// attribute
|
|
node = scene->CreateChild(node_id, magic::REPLICATED);
|
|
node->CreateComponent<SmoothedTransform>(magic::LOCAL);
|
|
}
|
|
// Read initial attributes
|
|
node->ReadDeltaUpdate(msg);
|
|
// Skip transition to first position
|
|
SmoothedTransform *transform = node->GetComponent<SmoothedTransform>();
|
|
if(transform)
|
|
transform->Update(1.0f, 0.0f);
|
|
|
|
// Read initial user variables
|
|
uint num_vars = msg.ReadVLE();
|
|
while(num_vars){
|
|
auto key = msg.ReadStringHash();
|
|
node->SetVar(key, msg.ReadVariant());
|
|
num_vars--;
|
|
}
|
|
|
|
// Read components
|
|
uint num_c = msg.ReadVLE();
|
|
while(num_c){
|
|
num_c--;
|
|
|
|
auto type = msg.ReadStringHash();
|
|
uint c_id = msg.ReadNetID();
|
|
|
|
Component *c = scene->GetComponent(c_id);
|
|
if(!c || c->GetType() != type || c->GetNode() != node){
|
|
if(c){
|
|
log_w(MODULE, "replicate:create_node: Component %i already"
|
|
" exists. This could be due to a component having"
|
|
" been accidentally created on the client side"
|
|
" without mode=LOCAL."
|
|
" If a component seems to mysteriously disappear,"
|
|
" this is the reason.", c_id);
|
|
c->Remove();
|
|
}
|
|
log_v(MODULE, "Creating component %i", c_id);
|
|
c = node->CreateComponent(type, magic::REPLICATED, c_id);
|
|
if(!c){
|
|
log_e(MODULE, "Could not create component %i", c_id);
|
|
return;
|
|
}
|
|
}
|
|
c->ReadDeltaUpdate(msg);
|
|
c->ApplyAttributes();
|
|
}
|
|
|
|
lua_State *L = m_app->get_lua();
|
|
lua_bindings::replicate::on_node_created(L, node_id);
|
|
};
|
|
|
|
m_packet_handlers["replicate:create_component"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
log_w("TODO: %s", cs(packet_name));
|
|
};
|
|
|
|
m_packet_handlers["replicate:latest_node_data"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
magic::Scene *scene = m_app->get_scene();
|
|
magic::MemoryBuffer msg(data.c_str(), data.size());
|
|
uint node_id = msg.ReadNetID();
|
|
Node *node = scene->GetNode(node_id);
|
|
if(node){
|
|
log_d(MODULE, "Updating node %i (LatestDataUpdate)", node_id);
|
|
node->ReadLatestDataUpdate(msg);
|
|
} else {
|
|
log_w(MODULE, "Out-of-order node data ignored for %i", node_id);
|
|
// Note: Network/Connection.cpp would buffer this
|
|
}
|
|
};
|
|
|
|
m_packet_handlers["replicate:latest_component_data"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
magic::Scene *scene = m_app->get_scene();
|
|
magic::MemoryBuffer msg(data.c_str(), data.size());
|
|
uint c_id = msg.ReadNetID();
|
|
Component *c = scene->GetComponent(c_id);
|
|
if(c){
|
|
log_d(MODULE, "Updating component %i (LatestDataUpdate)", c_id);
|
|
c->ReadLatestDataUpdate(msg);
|
|
c->ApplyAttributes();
|
|
} else {
|
|
log_w(MODULE, "Out-of-order component data ignored for %i", c_id);
|
|
// Note: Network/Connection.cpp would buffer this
|
|
}
|
|
};
|
|
|
|
m_packet_handlers["replicate:node_delta_update"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
magic::Scene *scene = m_app->get_scene();
|
|
magic::MemoryBuffer msg(data.c_str(), data.size());
|
|
uint node_id = msg.ReadNetID();
|
|
Node *node = scene->GetNode(node_id);
|
|
if(node){
|
|
log_d(MODULE, "Updating node %i (DeltaUpdate)", node_id);
|
|
node->ReadDeltaUpdate(msg);
|
|
} else {
|
|
log_w(MODULE, "Out-of-order node data ignored for %i", node_id);
|
|
// Note: Network/Connection.cpp would NOT buffer this
|
|
}
|
|
// Read user variables
|
|
uint num_vars = msg.ReadVLE();
|
|
while(num_vars){
|
|
auto key = msg.ReadStringHash();
|
|
node->SetVar(key, msg.ReadVariant());
|
|
num_vars--;
|
|
}
|
|
};
|
|
|
|
m_packet_handlers["replicate:component_delta_update"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
log_w(MODULE, "TODO: %s", cs(packet_name));
|
|
// Note: Network/Connection.cpp would NOT buffer this
|
|
};
|
|
|
|
m_packet_handlers["replicate:remove_node"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
log_w(MODULE, "TODO: %s", cs(packet_name));
|
|
};
|
|
|
|
m_packet_handlers["replicate:remove_component"] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
log_w(MODULE, "TODO: %s", cs(packet_name));
|
|
};
|
|
|
|
m_packet_handlers[""] =
|
|
[this](const ss_ &packet_name, const ss_ &data)
|
|
{
|
|
};
|
|
}
|
|
|
|
State* createState(sp_<app::App> app)
|
|
{
|
|
return new CState(app);
|
|
}
|
|
}
|
|
// vim: set noet ts=4 sw=4:
|