Newer
Older
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
Perttu Ahola
committed
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Perttu Ahola
committed
GNU Lesser General Public License for more details.
Perttu Ahola
committed
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <algorithm>
#include <sstream>
#include <IFileSystem.h>
#include "util/directiontables.h"
#include "util/pointedthing.h"
#include "util/serialize.h"
#include "util/string.h"
#include "client.h"
#include "filesys.h"
#include "mapblock_mesh.h"
#include "mapblock.h"
#include "settings.h"
#include "profiler.h"
#include "log.h"
#include "nodemetadata.h"
#include "itemdef.h"
#include "clientmedia.h"
MirceaKitsune
committed
#include "IMeshCache.h"
#include "config.h"
#include "database-sqlite3.h"
#include "serialization.h"
extern gui::IGUIEnvironment* guienv;
/*
QueuedMeshUpdate
*/
QueuedMeshUpdate::QueuedMeshUpdate():
p(-1337,-1337,-1337),
data(NULL),
ack_block_to_server(false)
{
}
QueuedMeshUpdate::~QueuedMeshUpdate()
{
if(data)
delete data;
}
/*
MeshUpdateQueue
*/
MeshUpdateQueue::MeshUpdateQueue()
{
}
MeshUpdateQueue::~MeshUpdateQueue()
{
JMutexAutoLock lock(m_mutex);
Kahrl
committed
for(std::vector<QueuedMeshUpdate*>::iterator
i = m_queue.begin();
i != m_queue.end(); i++)
{
QueuedMeshUpdate *q = *i;
delete q;
}
}
/*
peer_id=0 adds with nobody to send to
*/
Kahrl
committed
void MeshUpdateQueue::addBlock(v3s16 p, MeshMakeData *data, bool ack_block_to_server, bool urgent)
assert(data); // pre-condition
Kahrl
committed
if(urgent)
m_urgents.insert(p);
/*
Find if block is already in queue.
If it is, update the data and quit.
*/
Kahrl
committed
for(std::vector<QueuedMeshUpdate*>::iterator
i = m_queue.begin();
i != m_queue.end(); i++)
{
QueuedMeshUpdate *q = *i;
if(q->p == p)
{
if(q->data)
delete q->data;
q->data = data;
if(ack_block_to_server)
q->ack_block_to_server = true;
return;
}
}
/*
Add the block
*/
QueuedMeshUpdate *q = new QueuedMeshUpdate;
q->p = p;
q->data = data;
q->ack_block_to_server = ack_block_to_server;
m_queue.push_back(q);
}
// Returned pointer must be deleted
// Returns NULL if queue is empty
QueuedMeshUpdate * MeshUpdateQueue::pop()
{
JMutexAutoLock lock(m_mutex);
Kahrl
committed
bool must_be_urgent = !m_urgents.empty();
for(std::vector<QueuedMeshUpdate*>::iterator
i = m_queue.begin();
i != m_queue.end(); i++)
{
QueuedMeshUpdate *q = *i;
if(must_be_urgent && m_urgents.count(q->p) == 0)
continue;
m_queue.erase(i);
m_urgents.erase(q->p);
return q;
}
return NULL;
Perttu Ahola
committed
void * MeshUpdateThread::Thread()
log_register_thread("MeshUpdateThread");
BEGIN_DEBUG_EXCEPTION_HANDLER
Perttu Ahola
committed
porting::setThreadName("MeshUpdateThread");
while(!StopRequested())
Perttu Ahola
committed
QueuedMeshUpdate *q = m_queue_in.pop();
if(q == NULL)
{
Perttu Ahola
committed
sleep_ms(3);
Perttu Ahola
committed
continue;
}
ScopeProfiler sp(g_profiler, "Client: Mesh making");
Perttu Ahola
committed
MapBlockMesh *mesh_new = new MapBlockMesh(q->data, m_camera_offset);
Kahrl
committed
if(mesh_new->getMesh()->getMeshBufferCount() == 0)
{
delete mesh_new;
mesh_new = NULL;
}
Perttu Ahola
committed
MeshUpdateResult r;
r.p = q->p;
r.mesh = mesh_new;
r.ack_block_to_server = q->ack_block_to_server;
Perttu Ahola
committed
m_queue_out.push_back(r);
Perttu Ahola
committed
delete q;
END_DEBUG_EXCEPTION_HANDLER(errorstream)
/*
Client
*/
Client::Client(
IrrlichtDevice *device,
const char *playername,
std::string password,
Perttu Ahola
committed
MapDrawControl &control,
IWritableItemDefManager *itemdef,
m_packetcounter_timer(0.0),
m_connection_reinit_timer(0.1),
m_avg_rtt_timer(0.0),
m_playerpos_send_timer(0.0),
m_ignore_damage_timer(0.0),
Perttu Ahola
committed
m_tsrc(tsrc),
m_itemdef(itemdef),
Perttu Ahola
committed
new ClientMap(this, this, control,
device->getSceneManager()->getRootSceneNode(),
device->getSceneManager(), 666),
Perttu Ahola
committed
device->getSceneManager(),
sapier
committed
m_particle_manager(&m_env),
m_device(device),
m_server_ser_ver(SER_FMT_VER_INVALID),
m_inventory_from_server(NULL),
m_inventory_from_server_age(0.0),
m_show_highlighted(false),
Kahrl
committed
m_animation_time(0),
m_crack_level(-1),
m_crack_pos(0,0,0),
m_highlighted_pos(0,0,0),
m_map_seed(0),
m_password(password),
m_itemdef_received(false),
m_nodedef_received(false),
m_media_downloader(new ClientMediaDownloader()),
m_time_of_day_set(false),
m_last_time_of_day_f(-1),
m_recommended_send_interval(0.1),
m_removed_sounds_check_timer(0),
m_state(LC_Created),
m_localdb(NULL)
// Add local player
m_env.addPlayer(new LocalPlayer(this, playername));
m_cache_save_interval = g_settings->getU16("server_map_save_interval");
m_cache_smooth_lighting = g_settings->getBool("smooth_lighting");
m_cache_enable_shaders = g_settings->getBool("enable_shaders");
void Client::Stop()
{
//request all client managed threads to stop
m_mesh_update_thread.Stop();
// Save local server map
if (m_localdb) {
infostream << "Local map saving ended." << std::endl;
m_localdb->endSave();
}
bool Client::isShutdown()
{
if (!m_mesh_update_thread.IsRunning()) return true;
return false;
}
m_mesh_update_thread.Stop();
m_mesh_update_thread.Wait();
while(!m_mesh_update_thread.m_queue_out.empty()) {
MeshUpdateResult r = m_mesh_update_thread.m_queue_out.pop_frontNoEx();
delete m_inventory_from_server;
for(std::map<std::string, Inventory*>::iterator
i = m_detached_inventories.begin();
i != m_detached_inventories.end(); i++){
delete i->second;
Ilya Zhuravlev
committed
// cleanup 3d model meshes on client shutdown
while (m_device->getSceneManager()->getMeshCache()->getMeshCount() != 0) {
scene::IAnimatedMesh * mesh =
m_device->getSceneManager()->getMeshCache()->getMeshByIndex(0);
if (mesh != NULL)
m_device->getSceneManager()->getMeshCache()->removeMesh(mesh);
}
void Client::connect(Address address,
const std::string &address_name,
bool is_local_server)
initLocalMapSaving(address, address_name, is_local_server);
m_con.SetTimeoutMs(0);
m_con.Connect(address);
}
void Client::step(float dtime)
{
DSTACK(__FUNCTION_NAME);
Perttu Ahola
committed
if(m_ignore_damage_timer > dtime)
m_ignore_damage_timer -= dtime;
else
m_ignore_damage_timer = 0.0;
Kahrl
committed
m_animation_time += dtime;
if(m_animation_time > 60.0)
m_animation_time -= 60.0;
m_time_of_day_update_timer += dtime;
/*
Packet counter
*/
{
counter -= dtime;
if(counter <= 0.0)
{
infostream << "Client packetcounter (" << m_packetcounter_timer
<< "):"<<std::endl;
m_packetcounter.print(infostream);
m_packetcounter.clear();
}
}
// UGLY hack to fix 2 second startup delay caused by non existent
// server client startup synchronization in local server or singleplayer mode
static bool initial_step = true;
if (initial_step) {
initial_step = false;
}
else if(m_state == LC_Created) {
float &counter = m_connection_reinit_timer;
if(counter <= 0.0) {
Player *myplayer = m_env.getLocalPlayer();
FATAL_ERROR_IF(myplayer == NULL, "Local player not found in environment.");
// [23] u8[28] password (new in some version)
// [51] u16 minimum supported network protocol version (added sometime)
// [53] u16 maximum supported network protocol version (added later than the previous one)
char pName[PLAYERNAME_SIZE];
char pPassword[PASSWORD_SIZE];
memset(pName, 0, PLAYERNAME_SIZE * sizeof(char));
memset(pPassword, 0, PASSWORD_SIZE * sizeof(char));
snprintf(pName, PLAYERNAME_SIZE, "%s", myplayer->getName());
snprintf(pPassword, PASSWORD_SIZE, "%s", m_password.c_str());
sendLegacyInit(pName, pPassword);
}
// Not connected, return
return;
}
/*
Do stuff if connected
*/
/*
Run Map's timers and unload unused data
*/
const float map_timer_and_unload_dtime = 5.25;
if(m_map_timer_and_unload_interval.step(dtime, map_timer_and_unload_dtime)) {
ScopeProfiler sp(g_profiler, "Client: map timer and unload");
std::vector<v3s16> deleted_blocks;
m_env.getMap().timerUpdate(map_timer_and_unload_dtime,
g_settings->getFloat("client_unload_unused_data_timeout"),
/*
Send info to server
NOTE: This loop is intentionally iterated the way it is.
*/
std::vector<v3s16>::iterator i = deleted_blocks.begin();
std::vector<v3s16> sendlist;
for(;;) {
if(sendlist.size() == 255 || i == deleted_blocks.end()) {
if(sendlist.empty())
break;
/*
[0] u16 command
[2] u8 count
[3] v3s16 pos_0
[3+6] v3s16 pos_1
...
*/
sendDeletedBlocks(sendlist);
if(i == deleted_blocks.end())
break;
sendlist.clear();
}
sendlist.push_back(*i);
Perttu Ahola
committed
/*
Handle environment
*/
// Control local player (0ms)
LocalPlayer *player = m_env.getLocalPlayer();
assert(player != NULL);
player->applyControl(dtime);
// Step environment
m_env.step(dtime);
Perttu Ahola
committed
/*
Get events
*/
for(;;) {
ClientEnvEvent event = m_env.getClientEvent();
if(event.type == CEE_NONE) {
break;
}
else if(event.type == CEE_PLAYER_DAMAGE) {
if(m_ignore_damage_timer <= 0) {
u8 damage = event.player_damage.amount;
if(event.player_damage.send_to_server)
sendDamage(damage);
// Add to ClientEvent queue
ClientEvent event;
event.type = CE_PLAYER_DAMAGE;
event.player_damage.amount = damage;
m_client_event_queue.push(event);
Perttu Ahola
committed
}
else if(event.type == CEE_PLAYER_BREATH) {
u16 breath = event.player_breath.amount;
sendBreath(breath);
}
Perttu Ahola
committed
}
Perttu Ahola
committed
/*
Print some info
*/
float &counter = m_avg_rtt_timer;
counter += dtime;
if(counter >= 10) {
counter = 0.0;
// connectedAndInitialized() is true, peer exists.
float avg_rtt = getRTT();
infostream << "Client: avg_rtt=" << avg_rtt << std::endl;
Perttu Ahola
committed
/*
Send player position to server
*/
if((m_state == LC_Ready) && (counter >= m_recommended_send_interval))
Perttu Ahola
committed
/*
Replace updated meshes
*/
while(!m_mesh_update_thread.m_queue_out.empty())
Perttu Ahola
committed
{
MeshUpdateResult r = m_mesh_update_thread.m_queue_out.pop_frontNoEx();
Perttu Ahola
committed
MapBlock *block = m_env.getMap().getBlockNoCreateNoEx(r.p);
if(block) {
Kahrl
committed
// Delete the old mesh
if(block->mesh != NULL)
{
// TODO: Remove hardware buffers of meshbuffers of block->mesh
delete block->mesh;
block->mesh = NULL;
}
// Replace with the new mesh
block->mesh = r.mesh;
Perttu Ahola
committed
}
if(r.ack_block_to_server) {
Perttu Ahola
committed
/*
Acknowledge block
[0] u8 count
[1] v3s16 pos_0
Perttu Ahola
committed
*/
sendGotBlocks(r.p);
Perttu Ahola
committed
}
}
if(num_processed_meshes > 0)
g_profiler->graphAdd("num_processed_meshes", num_processed_meshes);
Perttu Ahola
committed
}
/*
Load fetched media
*/
if (m_media_downloader && m_media_downloader->isStarted()) {
m_media_downloader->step(this);
if (m_media_downloader->isDone()) {
delete m_media_downloader;
m_media_downloader = NULL;
}
}
/*
If the server didn't update the inventory in a while, revert
the local inventory (so the player notices the lag problem
and knows something is wrong).
*/
if(m_inventory_from_server)
{
float interval = 10.0;
float count_before = floor(m_inventory_from_server_age / interval);
m_inventory_from_server_age += dtime;
float count_after = floor(m_inventory_from_server_age / interval);
if(count_after != count_before)
{
// Do this every <interval> seconds after TOCLIENT_INVENTORY
// Reset the locally changed inventory to the authoritative inventory
Player *player = m_env.getLocalPlayer();
player->inventory = *m_inventory_from_server;
m_inventory_updated = true;
}
}
/*
Update positions of sounds attached to objects
*/
{
for(std::map<int, u16>::iterator
i = m_sounds_to_objects.begin();
i != m_sounds_to_objects.end(); i++)
{
int client_id = i->first;
u16 object_id = i->second;
ClientActiveObject *cao = m_env.getActiveObject(object_id);
if(!cao)
continue;
v3f pos = cao->getPosition();
m_sound->updateSoundPosition(client_id, pos);
}
}
/*
Handle removed remotely initiated sounds
*/
m_removed_sounds_check_timer += dtime;
if(m_removed_sounds_check_timer >= 2.32) {
m_removed_sounds_check_timer = 0;
// Find removed sounds and clear references to them
std::vector<s32> removed_server_ids;
for(std::map<s32, int>::iterator
i = m_sounds_server_to_client.begin();
i != m_sounds_server_to_client.end();) {
s32 server_id = i->first;
int client_id = i->second;
i++;
if(!m_sound->soundExists(client_id)) {
m_sounds_server_to_client.erase(server_id);
m_sounds_client_to_server.erase(client_id);
m_sounds_to_objects.erase(client_id);
removed_server_ids.push_back(server_id);
if(!removed_server_ids.empty()) {
sendRemovedSounds(removed_server_ids);
// Write server map
if (m_localdb && m_localdb_save_interval.step(dtime,
m_cache_save_interval)) {
m_localdb->endSave();
m_localdb->beginSave();
}
bool Client::loadMedia(const std::string &data, const std::string &filename)
{
// Silly irrlicht's const-incorrectness
Buffer<char> data_rw(data.c_str(), data.size());
std::string name;
const char *image_ext[] = {
".png", ".jpg", ".bmp", ".tga",
".pcx", ".ppm", ".psd", ".wal", ".rgb",
NULL
};
name = removeStringEnd(filename, image_ext);
if(name != "")
{
verbosestream<<"Client: Attempting to load image "
<<"file \""<<filename<<"\""<<std::endl;
io::IFileSystem *irrfs = m_device->getFileSystem();
video::IVideoDriver *vdrv = m_device->getVideoDriver();
// Create an irrlicht memory file
io::IReadFile *rfile = irrfs->createMemoryReadFile(
*data_rw, data_rw.getSize(), "_tempreadfile");
FATAL_ERROR_IF(!rfile, "Could not create irrlicht memory file.");
// Read image
video::IImage *img = vdrv->createImageFromFile(rfile);
if(!img){
errorstream<<"Client: Cannot create image from data of "
<<"file \""<<filename<<"\""<<std::endl;
rfile->drop();
return false;
}
else {
m_tsrc->insertSourceImage(filename, img);
img->drop();
rfile->drop();
return true;
}
}
const char *sound_ext[] = {
".0.ogg", ".1.ogg", ".2.ogg", ".3.ogg", ".4.ogg",
".5.ogg", ".6.ogg", ".7.ogg", ".8.ogg", ".9.ogg",
".ogg", NULL
};
name = removeStringEnd(filename, sound_ext);
if(name != "")
{
verbosestream<<"Client: Attempting to load sound "
<<"file \""<<filename<<"\""<<std::endl;
m_sound->loadSoundData(name, data);
return true;
}
const char *model_ext[] = {
MirceaKitsune
committed
".x", ".b3d", ".md2", ".obj",
NULL
};
name = removeStringEnd(filename, model_ext);
if(name != "")
{
verbosestream<<"Client: Storing model into memory: "
MirceaKitsune
committed
<<"\""<<filename<<"\""<<std::endl;
if(m_mesh_data.count(filename))
errorstream<<"Multiple models with name \""<<filename.c_str()
<<"\" found; replacing previous model"<<std::endl;
m_mesh_data[filename] = data;
return true;
}
errorstream<<"Client: Don't know how to load file \""
<<filename<<"\""<<std::endl;
return false;
}
// Virtual methods from con::PeerHandler
void Client::peerAdded(con::Peer *peer)
{
infostream<<"Client::peerAdded(): peer->id="
<<peer->id<<std::endl;
}
void Client::deletingPeer(con::Peer *peer, bool timeout)
{
infostream<<"Client::deletingPeer(): "
"Server Peer is getting deleted "
<<"(timeout="<<timeout<<")"<<std::endl;
}
/*
u16 command
u16 number of files requested
for each file {
u16 length of name
string name
}
*/
void Client::request_media(const std::vector<std::string> &file_requests)
{
std::ostringstream os(std::ios_base::binary);
writeU16(os, TOSERVER_REQUEST_MEDIA);
FATAL_ERROR_IF(file_requests_size > 0xFFFF, "Unsupported number of file requests");
// Packet dynamicly resized
NetworkPacket pkt(TOSERVER_REQUEST_MEDIA, 2 + 0);
pkt << (u16) (file_requests_size & 0xFFFF);
for(std::vector<std::string>::const_iterator i = file_requests.begin();
}
infostream << "Client: Sending media request list to server ("
<< file_requests.size() << " files. packet size)" << std::endl;
}
void Client::received_media()
{
NetworkPacket pkt(TOSERVER_RECEIVED_MEDIA, 0);
Send(&pkt);
infostream << "Client: Notifying server that we received all media"
<< std::endl;
void Client::initLocalMapSaving(const Address &address,
const std::string &hostname,
bool is_local_server)
{
if (!g_settings->getBool("enable_local_map_saving") || is_local_server) {
const std::string world_path = porting::path_user
+ DIR_DELIM + "worlds"
+ DIR_DELIM + "server_"
+ hostname + "_" + to_string(address.getPort());
fs::CreateAllDirs(world_path);
m_localdb = new Database_SQLite3(world_path);
m_localdb->beginSave();
actionstream << "Local map saving started, map will be saved at '" << world_path << "'" << std::endl;
}
void Client::ReceiveAll()
{
DSTACK(__FUNCTION_NAME);
// Limit time even if there would be huge amounts of data to
// process
if(porting::getTimeMs() > start_ms + 100)
break;
catch(con::NoIncomingDataException &e) {
catch(con::InvalidIncomingDataException &e) {
infostream<<"Client::ReceiveAll(): "
"InvalidIncomingDataException: what()="
<<e.what()<<std::endl;
}
}
}
void Client::Receive()
{
DSTACK(__FUNCTION_NAME);
SharedBuffer<u8> data;
inline void Client::handleCommand(NetworkPacket* pkt)
{
const ToClientCommandHandler& opHandle = toClientCommandTable[pkt->getCommand()];
(this->*opHandle.handler)(pkt);
}
/*
sender_peer_id given to this shall be quaranteed to be a valid peer
*/
void Client::ProcessData(u8 *data, u32 datasize, u16 sender_peer_id)
{
DSTACK(__FUNCTION_NAME);
// Ignore packets that don't even fit a command
if(datasize < 2) {
m_packetcounter.add(60000);
return;
Perttu Ahola
committed
}
NetworkPacket pkt(data, datasize, sender_peer_id);
ToClientCommand command = (ToClientCommand) pkt.getCommand();
//infostream<<"Client: received command="<<command<<std::endl;
m_packetcounter.add((u16)command);
/*
If this check is removed, be sure to change the queue
system to know the ids
*/
if(sender_peer_id != PEER_ID_SERVER) {
infostream << "Client::ProcessData(): Discarding data not "
"coming from server: peer_id=" << sender_peer_id
<< std::endl;
return;
// Command must be handled into ToClientCommandHandler
if (command >= TOCLIENT_NUM_MSG_TYPES) {
infostream << "Client: Ignoring unknown command "
<< command << std::endl;
}
/*
* Those packets are handled before m_server_ser_ver is set, it's normal
* But we must use the new ToClientConnectionState in the future,
* as a byte mask
*/
if(toClientCommandTable[command].state == TOCLIENT_STATE_NOT_CONNECTED) {
if(m_server_ser_ver == SER_FMT_VER_INVALID) {
infostream << "Client: Server serialization"
" format invalid or not initialized."
" Skipping incoming command=" << command << std::endl;
return;
/*
Handle runtime commands
*/
void Client::Send(NetworkPacket* pkt)
m_con.Send(PEER_ID_SERVER,
serverCommandFactoryTable[pkt->getCommand()].channel,
pkt,
serverCommandFactoryTable[pkt->getCommand()].reliable);
void Client::interact(u8 action, const PointedThing& pointed)
if(m_state != LC_Ready) {
errorstream << "Client::interact() "
"Canceled (not connected)"
<< std::endl;
[3] u16 item
[5] u32 length of the next item
[9] serialized PointedThing
0: start digging (from undersurface) or use
1: stop digging (all parameters ignored)
2: digging completed
3: place block or item (to abovesurface)
4: use item
NetworkPacket pkt(TOSERVER_INTERACT, 1 + 2 + 0);
pkt << action;
pkt << (u16)getPlayerItem();
std::ostringstream tmp_os(std::ios::binary);
pointed.serialize(tmp_os);
pkt.putLongString(tmp_os.str());
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
void Client::sendLegacyInit(const char* playerName, const char* playerPassword)
{
NetworkPacket pkt(TOSERVER_INIT_LEGACY,
1 + PLAYERNAME_SIZE + PASSWORD_SIZE + 2 + 2);
pkt << (u8) SER_FMT_VER_HIGHEST_READ;
pkt.putRawString(playerName,PLAYERNAME_SIZE);
pkt.putRawString(playerPassword, PASSWORD_SIZE);
pkt << (u16) CLIENT_PROTOCOL_VERSION_MIN << (u16) CLIENT_PROTOCOL_VERSION_MAX;
Send(&pkt);
}
void Client::sendDeletedBlocks(std::vector<v3s16> &blocks)
{
NetworkPacket pkt(TOSERVER_DELETEDBLOCKS, 1 + sizeof(v3s16) * blocks.size());
pkt << (u8) blocks.size();
u32 k = 0;
for(std::vector<v3s16>::iterator
j = blocks.begin();
j != blocks.end(); ++j) {
pkt << *j;
k++;
}
Send(&pkt);
}
void Client::sendGotBlocks(v3s16 block)
{
NetworkPacket pkt(TOSERVER_GOTBLOCKS, 1 + 6);
pkt << (u8) 1 << block;
Send(&pkt);
}
void Client::sendRemovedSounds(std::vector<s32> &soundList)
{
size_t server_ids = soundList.size();
assert(server_ids <= 0xFFFF);
NetworkPacket pkt(TOSERVER_REMOVED_SOUNDS, 2 + server_ids * 4);
pkt << (u16) (server_ids & 0xFFFF);
for(std::vector<s32>::iterator i = soundList.begin();