From 696c623e9e841dde631573cf69ccaca3fa9de678 Mon Sep 17 00:00:00 2001 From: Perttu Ahola Date: Fri, 2 Oct 2015 08:20:56 +0300 Subject: [PATCH] Initial commit directly taken from the backup file minetest_10-10-24_16-33-41_wonderful.tar.gz --- .gitignore | 41 ++ Makefile | 55 ++ data/fontlucida.png | Bin 0 -> 17284 bytes data/grass.png | Bin 0 -> 910 bytes data/grass2.png | Bin 0 -> 493 bytes data/stone.png | Bin 0 -> 856 bytes data/tf.jpg | Bin 0 -> 39989 bytes data/water.png | Bin 0 -> 103 bytes makepackage_windows.sh | 26 + minetest.sln | 20 + minetest.vcproj | 318 +++++++++++ src/client.cpp | 513 +++++++++++++++++ src/client.h | 154 +++++ src/clientserver.h | 23 + src/common_irrlicht.h | 13 + src/connection.cpp | 1181 +++++++++++++++++++++++++++++++++++++++ src/connection.h | 440 +++++++++++++++ src/environment.cpp | 146 +++++ src/environment.h | 48 ++ src/exceptions.h | 73 +++ src/heightmap.cpp | 437 +++++++++++++++ src/heightmap.h | 420 ++++++++++++++ src/light.h | 38 ++ src/loadstatus.h | 144 +++++ src/main.cpp | 976 ++++++++++++++++++++++++++++++++ src/main.h | 26 + src/map.cpp | 1204 ++++++++++++++++++++++++++++++++++++++++ src/map.h | 380 +++++++++++++ src/mapblock.cpp | 448 +++++++++++++++ src/mapblock.h | 275 +++++++++ src/mapnode.h | 102 ++++ src/mapsector.cpp | 252 +++++++++ src/mapsector.h | 156 ++++++ src/player.cpp | 196 +++++++ src/player.h | 93 ++++ src/server.cpp | 313 +++++++++++ src/server.h | 71 +++ src/socket.cpp | 295 ++++++++++ src/socket.h | 98 ++++ src/test.cpp | 744 +++++++++++++++++++++++++ src/test.h | 7 + src/utility.cpp | 52 ++ src/utility.h | 193 +++++++ 43 files changed, 9971 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 data/fontlucida.png create mode 100644 data/grass.png create mode 100644 data/grass2.png create mode 100644 data/stone.png create mode 100644 data/tf.jpg create mode 100644 data/water.png create mode 100755 makepackage_windows.sh create mode 100644 minetest.sln create mode 100644 minetest.vcproj create mode 100644 src/client.cpp create mode 100644 src/client.h create mode 100644 src/clientserver.h create mode 100644 src/common_irrlicht.h create mode 100644 src/connection.cpp create mode 100644 src/connection.h create mode 100644 src/environment.cpp create mode 100644 src/environment.h create mode 100644 src/exceptions.h create mode 100644 src/heightmap.cpp create mode 100644 src/heightmap.h create mode 100644 src/light.h create mode 100644 src/loadstatus.h create mode 100644 src/main.cpp create mode 100644 src/main.h create mode 100644 src/map.cpp create mode 100644 src/map.h create mode 100644 src/mapblock.cpp create mode 100644 src/mapblock.h create mode 100644 src/mapnode.h create mode 100644 src/mapsector.cpp create mode 100644 src/mapsector.h create mode 100644 src/player.cpp create mode 100644 src/player.h create mode 100644 src/server.cpp create mode 100644 src/server.h create mode 100644 src/socket.cpp create mode 100644 src/socket.h create mode 100644 src/test.cpp create mode 100644 src/test.h create mode 100644 src/utility.cpp create mode 100644 src/utility.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a724ddc --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +## Generic ignorable patterns and files +.* +!.gitignore +*~ +*bak* +tags +*.vim +*.orig +*.rej + +## Files related to minetest development cycle +/*.patch + +## Non-static Minetest directories +/bin/ + +## Configuration/log files +debug.txt + +## Build files +CMakeFiles +Makefile +!/Makefile +cmake_install.cmake +CMakeCache.txt +CPackConfig.cmake +CPackSourceConfig.cmake +src/android_version.h +src/android_version_githash.h +src/cmake_config.h +src/cmake_config_githash.h +src/lua/build/ +locale/ +.directory +.kdev4/ +*.cbp +*.kdev4 +*.layout +*.o +*.a + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8cc0eae --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +# Makefile for Irrlicht Examples +# It's usually sufficient to change just the target name and source file list +# and be sure that CXX is set to a valid compiler +TARGET = test +SOURCE_FILES = connection.cpp environment.cpp client.cpp server.cpp socket.cpp mapblock.cpp mapsector.cpp heightmap.cpp map.cpp player.cpp utility.cpp main.cpp test.cpp +SOURCES = $(addprefix src/, $(SOURCE_FILES)) +OBJECTS = $(SOURCES:.cpp=.o) + +IRRLICHTPATH = ../irrlicht/irrlicht-1.7.1 +JTHREADPATH = ../jthread/jthread-1.2.1 + +CPPFLAGS = -I$(IRRLICHTPATH)/include -I/usr/X11R6/include -I$(JTHREADPATH)/src + +#CXXFLAGS = -O3 -ffast-math -Wall +#CXXFLAGS = -O3 --fast-math -Wall -g +#CXXFLAGS = -Wall -g -O0 +CXXFLAGS = -O2 --fast-math -Wall -g + +#Default target + +all: all_linux + +ifeq ($(HOSTTYPE), x86_64) +LIBSELECT=64 +endif + +# Target specific settings + +all_linux: LDFLAGS = -L/usr/X11R6/lib$(LIBSELECT) -L$(IRRLICHTPATH)/lib/Linux -L$(JTHREADPATH)/src/.libs -lIrrlicht -lGL -lXxf86vm -lXext -lX11 -ljthread +all_linux clean_linux: SYSTEM=Linux + +all_win32: LDFLAGS = -L$(IRRLICHTPATH)/lib/Win32-gcc -L$(JTHREADPATH)/Debug -lIrrlicht -lopengl32 -lm -ljthread +all_win32 clean_win32: SYSTEM=Win32-gcc +all_win32 clean_win32: SUF=.exe + +# Name of the binary - only valid for targets which set SYSTEM + +DESTPATH = bin/$(TARGET)$(SUF) + +# Build commands + +all_linux all_win32: $(DESTPATH) + +$(DESTPATH): $(OBJECTS) + $(CXX) -o $@ $(OBJECTS) $(LDFLAGS) + +.cpp.o: + $(CXX) -c -o $@ $< $(CPPFLAGS) $(CXXFLAGS) + +clean: clean_linux clean_win32 + +clean_linux clean_win32: + @$(RM) $(OBJECTS) $(DESTPATH) + +.PHONY: all all_win32 clean clean_linux clean_win32 diff --git a/data/fontlucida.png b/data/fontlucida.png new file mode 100644 index 0000000000000000000000000000000000000000..c63fa02b7ab511b4241f224123f4739eab333f5e GIT binary patch literal 17284 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 literal 0 HcmV?d00001 diff --git a/data/grass.png b/data/grass.png new file mode 100644 index 0000000000000000000000000000000000000000..56362053f0f2a0c0ff86cc0461cb48b40e6f0225 GIT binary patch literal 910 zcmV;919AL`P)NX+N@;5;g>t6@hJ|RhY)mJb zsEHbhF){IRZzlc&KJeL`LuBt@AGJ;5rO_*FAFYv! z=Mnwm3YF#shGJ(XJU}f`<;HaI0vn=DWQ3j`?1S4%oSLT8X zOX~DL5!(brP#p4iaaGhuQ|oxQ1xA zV#t>Nz5lVArPJ2QVIs%n2`62aEN{;DB3UCWbSZY6ipXK2%JOEB*i3};q>YbnjgTpn zDK$hgXGQc@3y0jmQ@zaH^(5(19^s?UWiu9ifgcz52?t&Hy?*ZfmSSRBq;jZZc~QhA z07Vy?O~+#sD7HEnwTzIbiZpPwxo}b`6C0btu%D4WIU(F@C2^|r{*4bg*EP1{1)?D* z!BIbt9u!!+U*xZ963JCV3J$TLIxJ`n9Yt%y&_ralla}7#@zOf7MdQ+>#4EFJ@zrNb zOvYLWf}hZkgsjSl7v~@cy67;c(5eQ`K?lD-E}*Ik6EppEM`?&w@4u%)3$S<5zE2x8GRuaT>~bH;{vPR$4Y#Iir7GP@6Zgu?4*xOy@6`_*xh}`+%+HapoL%W zR1o8T8u(moM90JIt(~*76UW;vP|8Ed6J&PMN1~D=o2wBSmN`9v*_*HO%@^DBhwF&K z@$(@%?c?cPnW?Z$dQ&4Z8e@0U#qqh$mtU{rl|9dS2)Ay_rt0t^IZin}Qs^CQM^T#; zOAQVl3baQ=JbE9sjs{zgp3rhbDf6#~@Wv;Y{{_k?4XP&^YCy!@K7i~_v%FlSj>7yq k0q!hj7#^4Z@yI^oZ}X;X-6)sV>Hq)$07*qoM6N<$g25ZEL;wH) literal 0 HcmV?d00001 diff --git a/data/grass2.png b/data/grass2.png new file mode 100644 index 0000000000000000000000000000000000000000..66b3e58b53084d8b2f836a7703cfc3e1952881f9 GIT binary patch literal 493 zcmVMQax+iP!v6u1O^cq3=+YV2&_r3n}!GkT12KST|9>j zo#OwH$z=3u|A^AR;Ae`+1BF&(dYUu@oW@0vK~n>T4yRRQ-|c>$bMLu>yN!rar_5Ub z0F>0HHwY?$|5k?@c#jwWR*OPO9i$$v#+6Av zQR*a_DD$?%3@Tw>Tzg9Dhi^N}9053+dOyQo+vVUC)3S%-SucorAMp)nFvLP;HD z2PuPVk${q#r#P~&O;LxM*E9MTOfr#VV&=wuO}H4VmDC1L*1uz!x5Vk1lT6g1R#GRK zFgiY~ML~LX_-}BR0x)x9%4N`BEsFVVOi}l1%-pz>cC{!dD<8rlAC6>m9imUN`vcaP zw%>fZ3bxCgWsaycfN|qd6GlGTA13~AyWE|eV{o@Q7NNh@kwi-B!RKe}*9ZT{>P`Or jHT*-D-&*EvlsbO_7EIIP5WdM%00000NkvXXu0mjfJBr!r literal 0 HcmV?d00001 diff --git a/data/stone.png b/data/stone.png new file mode 100644 index 0000000000000000000000000000000000000000..d64470675ef93a9e21acce5a85ba3ab8891d0610 GIT binary patch literal 856 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/client.cpp b/src/client.cpp new file mode 100644 index 0000000..a37aa2d --- /dev/null +++ b/src/client.cpp @@ -0,0 +1,513 @@ +#include "client.h" +#include "utility.h" +#include +#include "clientserver.h" +#include "jmutexautolock.h" +#include "main.h" + +#ifdef _WIN32 + #include + #define sleep_ms(x) Sleep(x) +#else + #include + #define sleep_ms(x) usleep(x*1000) +#endif + +/* + FIXME: This thread can access the environment at any time +*/ + +void * ClientUpdateThread::Thread() +{ + ThreadStarted(); + + while(getRun()) + { + m_client->AsyncProcessData(); + sleep_ms(200); + } + + return NULL; +} + +Client::Client(scene::ISceneManager* smgr, video::SMaterial *materials): + m_thread(this), + m_env(new ClientMap + (this, materials, smgr->getRootSceneNode(), smgr, 666), + dout_client), + m_con(PROTOCOL_ID, 512), + m_smgr(smgr) +{ + m_fetchblock_mutex.Init(); + m_incoming_queue_mutex.Init(); + m_env_mutex.Init(); + m_con_mutex.Init(); + + m_thread.Start(); + + { + JMutexAutoLock envlock(m_env_mutex); + m_env.getMap().StartUpdater(); + + Player *player = new Player(true, smgr->getRootSceneNode(), smgr, 0); + //f32 y = BS*2 + m_env.getMap().getGroundHeight(v2s16(0,0)); + f32 y = BS*2 + BS*20; + player->setPosition(v3f(0, y, 0)); + m_env.addPlayer(player); + } +} + +Client::~Client() +{ + m_thread.setRun(false); + while(m_thread.IsRunning()) + sleep_ms(100); +} + +void Client::connect(Address address) +{ + { + JMutexAutoLock lock(m_con_mutex); + m_con.setTimeoutMs(0); + m_con.Connect(address); + } +} + +void Client::step(float dtime) +{ + ReceiveAll(); + + bool connected = false; + { + JMutexAutoLock lock(m_con_mutex); + m_con.RunTimeouts(dtime); + connected = m_con.Connected(); + } + { + JMutexAutoLock lock(m_env_mutex); + m_env.step(dtime); + } + + /* + Send stuff if connected + */ + if(connected) + { + sendPlayerPos(dtime); + } + + /* + Clear old entries from fetchblock history + */ + { + JMutexAutoLock lock(m_fetchblock_mutex); + + core::list remove_queue; + core::map::Iterator i; + i = m_fetchblock_history.getIterator(); + for(; i.atEnd() == false; i++) + { + float value = i.getNode()->getValue(); + value += dtime; + i.getNode()->setValue(value); + if(value >= 60.0) + remove_queue.push_back(i.getNode()->getKey()); + } + core::list::Iterator j; + j = remove_queue.begin(); + for(; j != remove_queue.end(); j++) + { + m_fetchblock_history.remove(*j); + } + } +} + +void Client::ReceiveAll() +{ + for(;;){ + try{ + Receive(); + } + catch(con::NoIncomingDataException &e) + { + break; + } + catch(con::InvalidIncomingDataException &e) + { + dout_client<<"Client::ReceiveAll(): " + "InvalidIncomingDataException: what()=" + < data(data_maxsize); + u16 peer_id; + u32 datasize; + { + JMutexAutoLock lock(m_con_mutex); + datasize = m_con.Receive(peer_id, *data, data_maxsize); + } + ProcessData(*data, datasize, peer_id); +} + +void Client::ProcessData(u8 *data, u32 datasize, u16 peer_id) +{ + // Ignore packets that don't even fit a command + if(datasize < 2) + return; + + ToClientCommand command = (ToClientCommand)readU16(&data[0]); + + // Execute fast commands straight away + + if(command == TOCLIENT_REMOVENODE) + { + if(datasize < 8) + return; + v3s16 p; + p.X = readS16(&data[2]); + p.Y = readS16(&data[4]); + p.Z = readS16(&data[6]); + { + JMutexAutoLock envlock(m_env_mutex); + m_env.getMap().removeNodeAndUpdate(p); + } + } + else if(command == TOCLIENT_ADDNODE) + { + if(datasize < 8 + MapNode::serializedLength()) + return; + + v3s16 p; + p.X = readS16(&data[2]); + p.Y = readS16(&data[4]); + p.Z = readS16(&data[6]); + + MapNode n; + n.deSerialize(&data[8]); + + { + JMutexAutoLock envlock(m_env_mutex); + f32 light = m_env.getMap().getNode(p).light; + m_env.getMap().setNode(p, n); + m_env.getMap().nodeAddedUpdate(p, light); + } + } + else if(command == TOCLIENT_PLAYERPOS) + { + u16 our_peer_id; + { + JMutexAutoLock lock(m_con_mutex); + our_peer_id = m_con.GetPeerID(); + } + // Cancel if we don't have a peer id + if(our_peer_id == PEER_ID_NEW){ + dout_client<<"TOCLIENT_PLAYERPOS cancelled: " + "we have no peer id" + <timeout_counter = 0.0; + + v3s32 ps = readV3S32(&data[start+2]); + v3s32 ss = readV3S32(&data[start+2+12]); + v3f position((f32)ps.X/100., (f32)ps.Y/100., (f32)ps.Z/100.); + v3f speed((f32)ss.X/100., (f32)ss.Y/100., (f32)ss.Z/100.); + player->setPosition(position); + player->speed = speed; + + start += (2+12+12); + } + } //envlock + } + // Default to queueing it (for slow commands) + else + { + JMutexAutoLock lock(m_incoming_queue_mutex); + + IncomingPacket packet(data, datasize); + m_incoming_queue.push_back(packet); + } +} + +bool Client::AsyncProcessData() +{ +getdata: + IncomingPacket packet = getPacket(); + u8 *data = packet.m_data; + u32 datasize = packet.m_datalen; + + // An empty packet means queue is empty + if(data == NULL){ + return false; + } + + if(datasize < 2) + goto getdata; + + ToClientCommand command = (ToClientCommand)readU16(&data[0]); + + if(command == TOCLIENT_BLOCKDATA) + { + // Ignore too small packet + if(datasize < 8 + MapBlock::serializedLength()) + goto getdata; + + v3s16 p; + p.X = readS16(&data[2]); + p.Y = readS16(&data[4]); + p.Z = readS16(&data[6]); + + dout_client<<"Client: Thread: BLOCKDATA for (" + <getBlockNoCreate(p.Y); + block->deSerialize(&data[8]); + } + catch(InvalidPositionException &e) + { + MapBlock *block = new MapBlock(&m_env.getMap(), p); + block->deSerialize(&data[8]); + sector->insertBlock(block); + } + } //envlock + + } + else + { + dout_client<<"Client: Thread: Ingoring unknown command " + < data, bool reliable) +{ + JMutexAutoLock lock(m_con_mutex); + m_con.Send(PEER_ID_SERVER, channelnum, data, reliable); +} + +void Client::fetchBlock(v3s16 p) +{ + JMutexAutoLock conlock(m_con_mutex); + if(m_con.Connected() == false) + return; + + JMutexAutoLock lock(m_fetchblock_mutex); + + // If fetch request was recently sent, cancel + if(m_fetchblock_history.find(p) != NULL) + return; + + + con::Peer *peer = m_con.GetPeer(PEER_ID_SERVER); + con::Channel *channel = &(peer->channels[1]); + + // Don't allow endless amounts of buffered reliable packets + if(channel->incoming_reliables.size() >= 100) + return; + // Don't allow endless amounts of non-acked requests + if(channel->outgoing_reliables.size() >= 10) + return; + + m_fetchblock_history.insert(p, 0.0); + + SharedBuffer data(8); + writeU16(&data[0], TOSERVER_GETBLOCK); + writeS16(&data[2], p.X); + writeS16(&data[4], p.Y); + writeS16(&data[6], p.Z); + m_con.Send(PEER_ID_SERVER, 1, data, true); +} + +IncomingPacket Client::getPacket() +{ + JMutexAutoLock lock(m_incoming_queue_mutex); + + core::list::Iterator i; + // Refer to first one + i = m_incoming_queue.begin(); + + // If queue is empty, return empty packet + if(i == m_incoming_queue.end()){ + IncomingPacket packet; + return packet; + } + + // Pop out first packet and return it + IncomingPacket packet = *i; + m_incoming_queue.erase(i); + return packet; +} + +void Client::removeNode(v3s16 nodepos) +{ + // Test that the position exists + try{ + JMutexAutoLock envlock(m_env_mutex); + m_env.getMap().getNode(nodepos); + } + catch(InvalidPositionException &e) + { + // Fail silently + return; + } + + SharedBuffer data(8); + writeU16(&data[0], TOSERVER_REMOVENODE); + writeS16(&data[2], nodepos.X); + writeS16(&data[4], nodepos.Y); + writeS16(&data[6], nodepos.Z); + Send(0, data, true); +} + +void Client::addNode(v3s16 nodepos, MapNode n) +{ + // Test that the position exists + try{ + JMutexAutoLock envlock(m_env_mutex); + m_env.getMap().getNode(nodepos); + } + catch(InvalidPositionException &e) + { + // Fail silently + return; + } + + u8 datasize = 8 + MapNode::serializedLength(); + SharedBuffer data(datasize); + writeU16(&data[0], TOSERVER_ADDNODE); + writeS16(&data[2], nodepos.X); + writeS16(&data[4], nodepos.Y); + writeS16(&data[6], nodepos.Z); + n.serialize(&data[8]); + Send(0, data, true); +} + +void Client::sendPlayerPos(float dtime) +{ + JMutexAutoLock envlock(m_env_mutex); + + Player *myplayer = m_env.getLocalPlayer(); + if(myplayer == NULL) + return; + + u16 our_peer_id; + { + JMutexAutoLock lock(m_con_mutex); + our_peer_id = m_con.GetPeerID(); + } + + // Set peer id if not set already + if(myplayer->peer_id == PEER_ID_NEW) + myplayer->peer_id = our_peer_id; + // Check that an existing peer_id is the same as the connection's + assert(myplayer->peer_id == our_peer_id); + + // Update at reasonable intervals (0.2s) + static float counter = 0.0; + counter += dtime; + if(counter < 0.2) + return; + counter = 0.0; + + v3f pf = myplayer->getPosition(); + v3s32 position(pf.X*100, pf.Y*100, pf.Z*100); + v3f sf = myplayer->speed; + v3s32 speed(sf.X*100, sf.Y*100, sf.Z*100); + + /*static v3s32 oldpos(-104300300,-10004300,-13234344); + static v3s32 oldspeed(-1234344,-2323424,-2343241); + if(position == oldpos && speed == oldspeed) + return; + oldpos = position; + oldspeed = speed;*/ + + SharedBuffer data(2+12+12); + writeU16(&data[0], TOSERVER_PLAYERPOS); + writeV3S32(&data[2], position); + writeV3S32(&data[2+12], speed); + // Send as unreliable + Send(0, data, false); +} + +void Client::updateCamera(v3f pos, v3f dir) +{ + JMutexAutoLock envlock(m_env_mutex); + m_env.getMap().updateCamera(pos, dir); +} + +MapNode Client::getNode(v3s16 p) +{ + JMutexAutoLock envlock(m_env_mutex); + return m_env.getMap().getNode(p); +} + +Player * Client::getLocalPlayer() +{ + JMutexAutoLock envlock(m_env_mutex); + return m_env.getLocalPlayer(); +} + +core::list Client::getPlayers() +{ + JMutexAutoLock envlock(m_env_mutex); + return m_env.getPlayers(); +} + + diff --git a/src/client.h b/src/client.h new file mode 100644 index 0000000..c76fc59 --- /dev/null +++ b/src/client.h @@ -0,0 +1,154 @@ +#ifndef CLIENT_HEADER +#define CLIENT_HEADER + +#include "connection.h" +#include "environment.h" +#include "common_irrlicht.h" +#include "jmutex.h" + +class Client; + +class ClientUpdateThread : public JThread +{ + bool run; + JMutex run_mutex; + + Client *m_client; + +public: + + ClientUpdateThread(Client *client) : JThread(), run(true), m_client(client) + { + run_mutex.Init(); + } + + void * Thread(); + + bool getRun() + { + run_mutex.Lock(); + bool run_cached = run; + run_mutex.Unlock(); + return run_cached; + } + void setRun(bool a_run) + { + run_mutex.Lock(); + run = a_run; + run_mutex.Unlock(); + } +}; + +struct IncomingPacket +{ + IncomingPacket() + { + m_data = NULL; + m_datalen = 0; + m_refcount = NULL; + } + IncomingPacket(const IncomingPacket &a) + { + m_data = a.m_data; + m_datalen = a.m_datalen; + m_refcount = a.m_refcount; + if(m_refcount != NULL) + (*m_refcount)++; + } + IncomingPacket(u8 *data, u32 datalen) + { + m_data = new u8[datalen]; + memcpy(m_data, data, datalen); + m_datalen = datalen; + m_refcount = new s32(1); + } + ~IncomingPacket() + { + if(m_refcount != NULL){ + assert(*m_refcount > 0); + (*m_refcount)--; + if(*m_refcount == 0){ + if(m_data != NULL) + delete[] m_data; + } + } + } + /*IncomingPacket & operator=(IncomingPacket a) + { + m_data = a.m_data; + m_datalen = a.m_datalen; + m_refcount = a.m_refcount; + (*m_refcount)++; + return *this; + }*/ + u8 *m_data; + u32 m_datalen; + s32 *m_refcount; +}; + +class Client +{ +public: + /* + NOTE: Every public method should be thread-safe + */ + Client(scene::ISceneManager* smgr, video::SMaterial *materials); + ~Client(); + void connect(Address address); + /* + Stuff that references the environment is valid only as + long as this is called. (eg. Players) + */ + void step(float dtime); + + void ProcessData(u8 *data, u32 datasize, u16 peer_id); + // Returns true if something was done + bool AsyncProcessData(); + void Send(u16 channelnum, SharedBuffer data, bool reliable); + + // Initiates block transfer + void fetchBlock(v3s16 p); + + // Pops out a packet from the packet queue + IncomingPacket getPacket(); + + void removeNode(v3s16 nodepos); + void addNode(v3s16 nodepos, MapNode n); + + void updateCamera(v3f pos, v3f dir); + + MapNode getNode(v3s16 p); + + // Return value is valid until client is destroyed + Player * getLocalPlayer(); + // Return value is valid until step() + core::list getPlayers(); + +private: + + void ReceiveAll(); + void Receive(); + + // m_con_mutex must be locked when calling these + void sendPlayerPos(float dtime); + + ClientUpdateThread m_thread; + + Environment m_env; + JMutex m_env_mutex; + + con::Connection m_con; + JMutex m_con_mutex; + + core::map m_fetchblock_history; + //core::list m_fetchblock_queue; + JMutex m_fetchblock_mutex; + + core::list m_incoming_queue; + JMutex m_incoming_queue_mutex; + + scene::ISceneManager* m_smgr; +}; + +#endif + diff --git a/src/clientserver.h b/src/clientserver.h new file mode 100644 index 0000000..7622375 --- /dev/null +++ b/src/clientserver.h @@ -0,0 +1,23 @@ +#ifndef CLIENTSERVER_HEADER +#define CLIENTSERVER_HEADER + +#define PROTOCOL_ID 0x4f457403 + +enum ToClientCommand +{ + TOCLIENT_BLOCKDATA=0x20, + TOCLIENT_ADDNODE, + TOCLIENT_REMOVENODE, + TOCLIENT_PLAYERPOS +}; + +enum ToServerCommand +{ + TOSERVER_GETBLOCK=0x20, + TOSERVER_ADDNODE, + TOSERVER_REMOVENODE, + TOSERVER_PLAYERPOS +}; + +#endif + diff --git a/src/common_irrlicht.h b/src/common_irrlicht.h new file mode 100644 index 0000000..ecf7cc7 --- /dev/null +++ b/src/common_irrlicht.h @@ -0,0 +1,13 @@ +#ifndef COMMON_IRRLICHT_HEADER +#define COMMON_IRRLICHT_HEADER + +#include +using namespace irr; +typedef core::vector3df v3f; +typedef core::vector3d v3s16; +typedef core::vector2d v2s16; +typedef core::vector2d v2f32; +typedef core::vector3d v3s32; + +#endif + diff --git a/src/connection.cpp b/src/connection.cpp new file mode 100644 index 0000000..720b8d7 --- /dev/null +++ b/src/connection.cpp @@ -0,0 +1,1181 @@ +#include "connection.h" +#include "main.h" + +namespace con +{ + +BufferedPacket makePacket(Address &address, u8 *data, u32 datasize, + u32 protocol_id, u16 sender_peer_id, u8 channel) +{ + u32 packet_size = datasize + BASE_HEADER_SIZE; + BufferedPacket p(packet_size); + p.address = address; + + writeU32(&p.data[0], protocol_id); + writeU16(&p.data[4], sender_peer_id); + writeU8(&p.data[6], channel); + + memcpy(&p.data[BASE_HEADER_SIZE], data, datasize); + + return p; +} + +BufferedPacket makePacket(Address &address, SharedBuffer &data, + u32 protocol_id, u16 sender_peer_id, u8 channel) +{ + return makePacket(address, *data, data.getSize(), + protocol_id, sender_peer_id, channel); +} + +SharedBuffer makeOriginalPacket( + SharedBuffer data) +{ + u32 header_size = 1; + u32 packet_size = data.getSize() + header_size; + SharedBuffer b(packet_size); + + writeU8(&b[0], TYPE_ORIGINAL); + + memcpy(&b[header_size], *data, data.getSize()); + + return b; +} + +core::list > makeSplitPacket( + SharedBuffer data, + u32 chunksize_max, + u16 seqnum) +{ + // Chunk packets, containing the TYPE_SPLIT header + core::list > chunks; + + u32 chunk_header_size = 7; + u32 maximum_data_size = chunksize_max - chunk_header_size; + u32 start = 0; + u32 end = 0; + u32 chunk_num = 0; + do{ + end = start + maximum_data_size - 1; + if(end > data.getSize() - 1) + end = data.getSize() - 1; + + u32 payload_size = end - start + 1; + u32 packet_size = chunk_header_size + payload_size; + + SharedBuffer chunk(packet_size); + + writeU8(&chunk[0], TYPE_SPLIT); + writeU16(&chunk[1], seqnum); + // [3] u16 chunk_count is written at next stage + writeU16(&chunk[5], chunk_num); + memcpy(&chunk[chunk_header_size], &data[start], payload_size); + + chunks.push_back(chunk); + + start = end + 1; + chunk_num++; + } + while(end != data.getSize() - 1); + + u16 chunk_count = chunks.getSize(); + + core::list >::Iterator i = chunks.begin(); + for(; i != chunks.end(); i++) + { + // Write chunk_count + writeU16(&((*i)[3]), chunk_count); + } + + return chunks; +} + +core::list > makeAutoSplitPacket( + SharedBuffer data, + u32 chunksize_max, + u16 &split_seqnum) +{ + u32 original_header_size = 1; + core::list > list; + if(data.getSize() + original_header_size > chunksize_max) + { + list = makeSplitPacket(data, chunksize_max, split_seqnum); + split_seqnum++; + return list; + } + else + { + list.push_back(makeOriginalPacket(data)); + } + return list; +} + +SharedBuffer makeReliablePacket( + SharedBuffer data, + u16 seqnum) +{ + u32 header_size = 3; + u32 packet_size = data.getSize() + header_size; + SharedBuffer b(packet_size); + + writeU8(&b[0], TYPE_RELIABLE); + writeU16(&b[1], seqnum); + + memcpy(&b[header_size], *data, data.getSize()); + + return b; +} + +/* + ReliablePacketBuffer +*/ + +void ReliablePacketBuffer::print() +{ + core::list::Iterator i; + i = m_list.begin(); + for(; i != m_list.end(); i++) + { + u16 s = readU16(&(i->data[BASE_HEADER_SIZE+1])); + dout_con<::Iterator i; + i = m_list.begin(); + for(; i != m_list.end(); i++) + { + u16 s = readU16(&(i->data[BASE_HEADER_SIZE+1])); + /*dout_con<<"findPacket(): finding seqnum="<::Iterator i = m_list.begin(); + m_list.erase(i); + return p; +} +BufferedPacket ReliablePacketBuffer::popSeqnum(u16 seqnum) +{ + RPBSearchResult r = findPacket(seqnum); + if(r == notFound()){ + dout_con<<"Not found"<= BASE_HEADER_SIZE+3); + u8 type = readU8(&p.data[BASE_HEADER_SIZE+0]); + assert(type == TYPE_RELIABLE); + u16 seqnum = readU16(&p.data[BASE_HEADER_SIZE+1]); + + // Find the right place for the packet and insert it there + + // If list is empty, just add it + if(m_list.empty()) + { + m_list.push_back(p); + // Done. + return; + } + // Otherwise find the right place + core::list::Iterator i; + i = m_list.begin(); + // Find the first packet in the list which has a higher seqnum + for(; i != m_list.end(); i++){ + u16 s = readU16(&(i->data[BASE_HEADER_SIZE+1])); + if(s == seqnum){ + throw AlreadyExistsException("Same seqnum in list"); + } + if(seqnum_higher(s, seqnum)){ + break; + } + } + // If we're at the end of the list, add the packet to the + // end of the list + if(i == m_list.end()) + { + m_list.push_back(p); + // Done. + return; + } + // Insert before i + m_list.insert_before(i, p); +} + +void ReliablePacketBuffer::incrementTimeouts(float dtime) +{ + core::list::Iterator i; + i = m_list.begin(); + for(; i != m_list.end(); i++){ + i->time += dtime; + i->totaltime += dtime; + } +} + +void ReliablePacketBuffer::resetTimedOuts(float timeout) +{ + core::list::Iterator i; + i = m_list.begin(); + for(; i != m_list.end(); i++){ + if(i->time >= timeout) + i->time = 0.0; + } +} + +core::list ReliablePacketBuffer::getTimedOuts(float timeout) +{ + core::list timed_outs; + core::list::Iterator i; + i = m_list.begin(); + for(; i != m_list.end(); i++) + { + if(i->time >= timeout) + timed_outs.push_back(*i); + } + return timed_outs; +} + +/* + IncomingSplitBuffer +*/ + +IncomingSplitBuffer::~IncomingSplitBuffer() +{ + core::map::Iterator i; + i = m_buf.getIterator(); + for(; i.atEnd() == false; i++) + { + delete i.getNode()->getValue(); + } +} +/* + This will throw a GotSplitPacketException when a full + split packet is constructed. +*/ +void IncomingSplitBuffer::insert(BufferedPacket &p, bool reliable) +{ + u32 headersize = BASE_HEADER_SIZE + 7; + assert(p.data.getSize() >= headersize); + u8 type = readU8(&p.data[BASE_HEADER_SIZE+0]); + assert(type == TYPE_SPLIT); + u16 seqnum = readU16(&p.data[BASE_HEADER_SIZE+1]); + u16 chunk_count = readU16(&p.data[BASE_HEADER_SIZE+3]); + u16 chunk_num = readU16(&p.data[BASE_HEADER_SIZE+5]); + + // Add if doesn't exist + if(m_buf.find(seqnum) == NULL) + { + IncomingSplitPacket *sp = new IncomingSplitPacket(); + sp->chunk_count = chunk_count; + sp->reliable = reliable; + m_buf[seqnum] = sp; + } + + IncomingSplitPacket *sp = m_buf[seqnum]; + + if(chunk_count != sp->chunk_count) + dout_con<<"WARNING: chunk_count="<chunk_count="<chunk_count + <reliable) + dout_con<<"WARNING: reliable="<reliable="<reliable + <chunks.find(chunk_num) != NULL) + throw AlreadyExistsException("Chunk already in buffer"); + + // Cut chunk data out of packet + u32 chunkdatasize = p.data.getSize() - headersize; + SharedBuffer chunkdata(chunkdatasize); + memcpy(*chunkdata, &(p.data[headersize]), chunkdatasize); + + // Set chunk data in buffer + sp->chunks[chunk_num] = chunkdata; + + // If not all chunks are received, return + if(sp->allReceived() == false) + return; + + // Calculate total size + u32 totalsize = 0; + core::map >::Iterator i; + i = sp->chunks.getIterator(); + for(; i.atEnd() == false; i++) + { + totalsize += i.getNode()->getValue().getSize(); + } + + SharedBuffer fulldata(totalsize); + + // Copy chunks to data buffer + u32 start = 0; + for(u32 chunk_i=0; chunk_ichunk_count; + chunk_i++) + { + SharedBuffer buf = sp->chunks[chunk_i]; + u16 chunkdatasize = buf.getSize(); + memcpy(&fulldata[start], *buf, chunkdatasize); + start += chunkdatasize;; + } + + // Remove sp from buffer + m_buf.remove(seqnum); + delete sp; + + throw GotSplitPacketException(fulldata); +} +void IncomingSplitBuffer::removeUnreliableTimedOuts(float dtime, float timeout) +{ + core::list remove_queue; + core::map::Iterator i; + i = m_buf.getIterator(); + for(; i.atEnd() == false; i++) + { + IncomingSplitPacket *p = i.getNode()->getValue(); + // Reliable ones are not removed by timeout + if(p->reliable == true) + continue; + p->time += dtime; + if(p->time >= timeout) + remove_queue.push_back(i.getNode()->getKey()); + } + core::list::Iterator j; + j = remove_queue.begin(); + for(; j != remove_queue.end(); j++) + { + dout_con<<"NOTE: Removing timed out unreliable split packet" + <::Iterator j; + j = m_peers.getIterator(); + for(; j.atEnd() == false; j++) + { + Peer *peer = j.getNode()->getValue(); + delete peer; + } +} + +void Connection::Serve(unsigned short port) +{ + m_socket.Bind(port); + m_peer_id = PEER_ID_SERVER; +} + +void Connection::Connect(Address address) +{ + core::map::Node *node = m_peers.find(PEER_ID_SERVER); + if(node != NULL){ + throw ConnectionException("Already connected to a server"); + } + + Peer *peer = new Peer(); + peer->address = address; + peer->id = PEER_ID_SERVER; + m_peers.insert(peer->id, peer); + + m_socket.Bind(0); + + // Send a dummy packet to server with peer_id = PEER_ID_NEW + m_peer_id = PEER_ID_NEW; + SharedBuffer data(0); + Send(PEER_ID_SERVER, 0, data, true); + + m_waiting_new_peer_id = true; +} + +bool Connection::Connected() +{ + if(m_peers.size() != 1) + return false; + + core::map::Node *node = m_peers.find(PEER_ID_SERVER); + if(node == NULL) + return false; + + if(m_peer_id == PEER_ID_NEW) + return false; + + return true; +} + +SharedBuffer Channel::ProcessPacket( + SharedBuffer packetdata, + Connection *con, + u16 peer_id, + u8 channelnum, + bool reliable) +{ + IndentationRaiser iraiser(&(con->m_indentation)); + + if(packetdata.getSize() < 1) + throw InvalidIncomingDataException("packetdata.getSize() < 1"); + + u8 type = readU8(&packetdata[0]); + + if(type == TYPE_CONTROL) + { + if(packetdata.getSize() < 2) + throw InvalidIncomingDataException("packetdata.getSize() < 2"); + + u8 controltype = readU8(&packetdata[1]); + + if(controltype == CONTROLTYPE_ACK) + { + if(packetdata.getSize() < 4) + throw InvalidIncomingDataException + ("packetdata.getSize() < 4 (ACK header size)"); + + u16 seqnum = readU16(&packetdata[2]); + con->PrintInfo(); + dout_con<<"Got CONTROLTYPE_ACK: channelnum=" + <<((int)channelnum&0xff)<<", peer_id="<PrintInfo(); + outgoing_reliables.print(); + dout_con<PrintInfo(); + dout_con<<"WARNING: ACKed packet not in outgoing queue" + <PrintInfo(); + dout_con<<"Got new peer id: "<GetPeerID() != PEER_ID_NEW) + { + dout_con<<"WARNING: not changing."<SetPeerID(peer_id_new); + } + throw ProcessedSilentlyException("Got a SET_PEER_ID"); + } + else if(controltype == CONTROLTYPE_PING) + { + // Just ignore it, the incoming data already reset + // the timeout counter + con->PrintInfo(); + dout_con<<"PING"<PrintInfo(); + dout_con<<"INVALID TYPE_CONTROL: invalid controltype=" + <<((int)controltype&0xff)<PrintInfo(); + dout_con<<"RETURNING TYPE_ORIGINAL to user" + < payload(packetdata.getSize() - ORIGINAL_HEADER_SIZE); + memcpy(*payload, &packetdata[ORIGINAL_HEADER_SIZE], payload.getSize()); + return payload; + } + else if(type == TYPE_SPLIT) + { + // We have to create a packet again for buffering + // This isn't actually too bad an idea. + BufferedPacket packet = makePacket( + con->GetPeer(peer_id)->address, + packetdata, + con->GetProtocolID(), + peer_id, + channelnum); + try{ + // Buffer the packet + incoming_splits.insert(packet, reliable); + } + // This exception happens when all the pieces of a packet + // are collected. + catch(GotSplitPacketException &e) + { + con->PrintInfo(); + dout_con<<"RETURNING TYPE_SPLIT: Constructed full data, " + <<"size="<PrintInfo(); + dout_con<<"BUFFERING TYPE_SPLIT"<PrintInfo(); + if(is_future_packet) + dout_con<<"BUFFERING"; + else if(is_old_packet) + dout_con<<"OLD"; + else + dout_con<<"RECUR"; + dout_con<<" TYPE_RELIABLE seqnum="< reply(4); + writeU8(&reply[0], TYPE_CONTROL); + writeU8(&reply[1], CONTROLTYPE_ACK); + writeU16(&reply[2], seqnum); + con->SendAsPacket(peer_id, channelnum, reply, false); + + //if(seqnum_higher(seqnum, next_incoming_seqnum)) + if(is_future_packet) + { + /*con->PrintInfo(); + dout_con<<"Buffering reliable packet (seqnum=" + <GetPeer(peer_id)->address, + packetdata, + con->GetProtocolID(), + peer_id, + channelnum); + try{ + incoming_reliables.insert(packet); + + /*con->PrintInfo(); + dout_con<<"INCOMING: "; + incoming_reliables.print(); + dout_con< payload(packetdata.getSize() - RELIABLE_HEADER_SIZE); + memcpy(*payload, &packetdata[RELIABLE_HEADER_SIZE], payload.getSize()); + + return ProcessPacket(payload, con, peer_id, channelnum, true); + } + else + { + con->PrintInfo(); + dout_con<<"Got invalid type="<<((int)type&0xff)< Channel::CheckIncomingBuffers(Connection *con, + u16 &peer_id) +{ + u16 firstseqnum = 0; + // Clear old packets from start of buffer + try{ + for(;;){ + firstseqnum = incoming_reliables.getFirstSeqnum(); + if(seqnum_higher(next_incoming_seqnum, firstseqnum)) + incoming_reliables.popFirst(); + else + break; + } + // This happens if all packets are old + }catch(con::NotFoundException) + {} + + if(incoming_reliables.empty() == false) + { + if(firstseqnum == next_incoming_seqnum) + { + BufferedPacket p = incoming_reliables.popFirst(); + + peer_id = readPeerId(*p.data); + u8 channelnum = readChannel(*p.data); + u16 seqnum = readU16(&p.data[BASE_HEADER_SIZE+1]); + + con->PrintInfo(); + dout_con<<"UNBUFFERING TYPE_RELIABLE" + <<" seqnum="< Connection::GetFromBuffers(u16 &peer_id) +{ + core::map::Iterator j; + j = m_peers.getIterator(); + for(; j.atEnd() == false; j++) + { + Peer *peer = j.getNode()->getValue(); + for(u16 i=0; ichannels[i]; + try{ + SharedBuffer resultdata = channel->CheckIncomingBuffers + (this, peer_id); + + return resultdata; + } + catch(NoIncomingDataException &e) + { + } + catch(InvalidIncomingDataException &e) + { + } + catch(ProcessedSilentlyException &e) + { + } + } + } + throw NoIncomingDataException("No relevant data in buffers"); +} + +u32 Connection::Receive(u16 &peer_id, u8 *data, u32 datasize) +{ + /* + Receive a packet from the network + */ + + // TODO: We can not know how many layers of header there are. + // For now, just assume there are no other than the base headers. + u32 packet_maxsize = datasize + BASE_HEADER_SIZE; + Buffer packetdata(packet_maxsize); + + for(;;) + { + try + { + /* + Check if some buffer has relevant data + */ + try{ + SharedBuffer resultdata = GetFromBuffers(peer_id); + + if(datasize < resultdata.getSize()) + throw InvalidIncomingDataException + ("Buffer too small for received data"); + + memcpy(data, *resultdata, resultdata.getSize()); + return resultdata.getSize(); + } + catch(NoIncomingDataException &e) + { + } + + Address sender; + + s32 received_size = m_socket.Receive(sender, *packetdata, packet_maxsize); + + if(received_size < 0) + throw NoIncomingDataException("No incoming data"); + if(received_size < BASE_HEADER_SIZE) + throw InvalidIncomingDataException("No full header received"); + if(readU32(&packetdata[0]) != m_protocol_id) + throw InvalidIncomingDataException("Invalid protocol id"); + + peer_id = readPeerId(*packetdata); + u8 channelnum = readChannel(*packetdata); + if(channelnum > CHANNEL_COUNT-1){ + PrintInfo(); + dout_con<<"Receive(): Invalid channel "<::Iterator j; + j = m_peers.getIterator(); + for(; j.atEnd() == false; j++) + { + Peer *peer = j.getNode()->getValue(); + if(peer->has_sent_with_id) + continue; + if(peer->address == sender) + break; + } + + /* + If no peer was found with the same address and port, + we shall assume it is a new peer and create an entry. + */ + if(j.atEnd()) + { + // Pass on to adding the peer + } + // Else: A peer was found. + else + { + Peer *peer = j.getNode()->getValue(); + peer_id = peer->id; + PrintInfo(); + dout_con<<"WARNING: Assuming unknown peer to be " + <<"peer_id="<::Iterator j; + j = m_peers.getIterator(); + for(; j.atEnd() == false; j++) + { + Peer *peer = j.getNode()->getValue(); + if(peer_id_new <= peer->id) + peer_id_new = peer->id + 1; + } + + PrintInfo(); + dout_con<<"Receive(): Got a packet with peer_id=PEER_ID_NEW," + " giving peer_id="<address = sender; + peer->id = peer_id_new; + m_peers.insert(peer->id, peer); + + // Create CONTROL packet to tell the peer id to the new peer. + SharedBuffer reply(4); + writeU8(&reply[0], TYPE_CONTROL); + writeU8(&reply[1], CONTROLTYPE_SET_PEER_ID); + writeU16(&reply[2], peer_id_new); + SendAsPacket(peer_id_new, 0, reply, true); + + // We're now talking to a valid peer_id + peer_id = peer_id_new; + + // Go on and process whatever it sent + } + + core::map::Node *node = m_peers.find(peer_id); + + if(node == NULL){ + // Peer not found + PrintInfo(); + dout_con<<"Receive(): Peer not found"<getValue(); + + //TODO: Validate peer address + + peer->timeout_counter = 0.0; + + Channel *channel = &(peer->channels[channelnum]); + + // Throw the received packet to channel->processPacket() + + // Make a new SharedBuffer from the data without the base headers + SharedBuffer strippeddata(received_size - BASE_HEADER_SIZE); + memcpy(*strippeddata, &packetdata[BASE_HEADER_SIZE], + strippeddata.getSize()); + + try{ + // Process it (the result is some data with no headers made by us) + SharedBuffer resultdata = channel->ProcessPacket + (strippeddata, this, peer_id, channelnum); + + PrintInfo(); + dout_con<<"ProcessPacket returned data of size " + < data, bool reliable) +{ + core::map::Iterator j; + j = m_peers.getIterator(); + for(; j.atEnd() == false; j++) + { + Peer *peer = j.getNode()->getValue(); + Send(peer->id, channelnum, data, reliable); + } +} + +void Connection::Send(u16 peer_id, u8 channelnum, + SharedBuffer data, bool reliable) +{ + assert(channelnum < CHANNEL_COUNT); + + Peer *peer = GetPeer(peer_id); + Channel *channel = &(peer->channels[channelnum]); + + u32 chunksize_max = m_max_packet_size - BASE_HEADER_SIZE; + if(reliable) + chunksize_max -= RELIABLE_HEADER_SIZE; + + core::list > originals; + originals = makeAutoSplitPacket(data, chunksize_max, + channel->next_outgoing_split_seqnum); + + core::list >::Iterator i; + i = originals.begin(); + for(; i != originals.end(); i++) + { + SharedBuffer original = *i; + + SendAsPacket(peer_id, channelnum, original, reliable); + } +} + +void Connection::SendAsPacket(u16 peer_id, u8 channelnum, + SharedBuffer data, bool reliable) +{ + Peer *peer = GetPeer(peer_id); + Channel *channel = &(peer->channels[channelnum]); + + if(reliable) + { + u16 seqnum = channel->next_outgoing_seqnum; + channel->next_outgoing_seqnum++; + + SharedBuffer reliable = makeReliablePacket(data, seqnum); + + // Add base headers and make a packet + BufferedPacket p = makePacket(peer->address, reliable, + m_protocol_id, m_peer_id, channelnum); + + try{ + // Buffer the packet + channel->outgoing_reliables.insert(p); + } + catch(AlreadyExistsException &e) + { + PrintInfo(); + dout_con<<"WARNING: Going to send a reliable packet " + "seqnum="<address, data, + m_protocol_id, m_peer_id, channelnum); + + // Send the packet + RawSend(p); + } +} + +void Connection::RawSend(const BufferedPacket &packet) +{ + m_socket.Send(packet.address, *packet.data, packet.data.getSize()); +} + +void Connection::RunTimeouts(float dtime) +{ + core::list timeouted_peers; + core::map::Iterator j; + j = m_peers.getIterator(); + for(; j.atEnd() == false; j++) + { + Peer *peer = j.getNode()->getValue(); + + // Check peer timeout + peer->timeout_counter += dtime; + // TODO: Get the constant timeout from somewhere else + if(peer->timeout_counter > 30.0) + { + PrintInfo(); + dout_con<<"RunTimeouts(): Peer "<id + <<" has timeouted."<id); + // Don't bother going through the buffers of this one + continue; + } + + peer->ping_timer += dtime; + if(peer->ping_timer >= 5.0) + { + // Create and send PING packet + SharedBuffer data(2); + writeU8(&data[0], TYPE_CONTROL); + writeU8(&data[1], CONTROLTYPE_PING); + SendAsPacket(peer->id, 0, data, true); + + peer->ping_timer = 0.0; + } + + float resend_timeout = peer->resend_timeout; + for(u16 i=0; ichannels[i]; + + // Re-send timed out outgoing reliables + + channel->outgoing_reliables.incrementTimeouts(dtime); + + core::list timed_outs + = channel->outgoing_reliables.getTimedOuts(resend_timeout); + + channel->outgoing_reliables.resetTimedOuts(resend_timeout); + + core::list::Iterator j = timed_outs.begin(); + for(; j != timed_outs.end(); j++) + { + u16 peer_id = readPeerId(*(j->data)); + u8 channel = readChannel(*(j->data)); + u16 seqnum = readU16(&(j->data[BASE_HEADER_SIZE+1])); + PrintInfo(); + dout_con<<"RE-SENDING timed-out RELIABLE: " + <<"peer_id="<::Node *node = m_peers.find(peer_id); + + if(node == NULL){ + // Peer not found + throw NotFoundException("Peer not found (possible timeout)"); + } + + // Error checking + assert(node->getValue()->id == peer_id); + + return node->getValue(); +} + +void Connection::PrintInfo() +{ + /*for(u16 i=0; i +#include "common_irrlicht.h" +#include "socket.h" +#include "utility.h" +#include "exceptions.h" +#include +#include + +namespace con +{ + +class NotFoundException : public BaseException +{ +public: + NotFoundException(const char *s): + BaseException(s) + {} +}; + +class ConnectionException : public BaseException +{ +public: + ConnectionException(const char *s): + BaseException(s) + {} +}; + +/*class ThrottlingException : public BaseException +{ +public: + ThrottlingException(const char *s): + BaseException(s) + {} +};*/ + +class InvalidIncomingDataException : public BaseException +{ +public: + InvalidIncomingDataException(const char *s): + BaseException(s) + {} +}; + +class InvalidOutgoingDataException : public BaseException +{ +public: + InvalidOutgoingDataException(const char *s): + BaseException(s) + {} +}; + +class NoIncomingDataException : public BaseException +{ +public: + NoIncomingDataException(const char *s): + BaseException(s) + {} +}; + +class ProcessedSilentlyException : public BaseException +{ +public: + ProcessedSilentlyException(const char *s): + BaseException(s) + {} +}; + +class GotSplitPacketException +{ + SharedBuffer m_data; +public: + GotSplitPacketException(SharedBuffer data): + m_data(data) + {} + SharedBuffer getData() + { + return m_data; + } +}; + +inline u16 readPeerId(u8 *packetdata) +{ + return readU16(&packetdata[4]); +} +inline u8 readChannel(u8 *packetdata) +{ + return readU8(&packetdata[6]); +} + +#define SEQNUM_MAX 65535 +inline bool seqnum_higher(u16 higher, u16 lower) +{ + if(lower > higher && lower - higher > SEQNUM_MAX/2){ + return true; + } + return (higher > lower); +} + +struct BufferedPacket +{ + BufferedPacket(u8 *a_data, u32 a_size): + data(a_data, a_size), time(0.0), totaltime(0.0) + {} + BufferedPacket(u32 a_size): + data(a_size), time(0.0), totaltime(0.0) + {} + SharedBuffer data; // Data of the packet, including headers + float time; // Seconds from buffering the packet or re-sending + float totaltime; // Seconds from buffering the packet + Address address; // Sender or destination +}; + +// This adds the base headers to the data and makes a packet out of it +BufferedPacket makePacket(Address &address, u8 *data, u32 datasize, + u32 protocol_id, u16 sender_peer_id, u8 channel); +BufferedPacket makePacket(Address &address, SharedBuffer &data, + u32 protocol_id, u16 sender_peer_id, u8 channel); + +// Add the TYPE_ORIGINAL header to the data +SharedBuffer makeOriginalPacket( + SharedBuffer data); + +// Split data in chunks and add TYPE_SPLIT headers to them +core::list > makeSplitPacket( + SharedBuffer data, + u32 chunksize_max, + u16 seqnum); + +// Depending on size, make a TYPE_ORIGINAL or TYPE_SPLIT packet +// Increments split_seqnum if a split packet is made +core::list > makeAutoSplitPacket( + SharedBuffer data, + u32 chunksize_max, + u16 &split_seqnum); + +// Add the TYPE_RELIABLE header to the data +SharedBuffer makeReliablePacket( + SharedBuffer data, + u16 seqnum); + +struct IncomingSplitPacket +{ + IncomingSplitPacket() + { + time = 0.0; + reliable = false; + } + // Key is chunk number, value is data without headers + core::map > chunks; + u32 chunk_count; + float time; // Seconds from adding + bool reliable; // If true, isn't deleted on timeout + + bool allReceived() + { + return (chunks.size() == chunk_count); + } +}; + +/* +=== NOTES === + +A packet is sent through a channel to a peer with a basic header: +TODO: Should we have a receiver_peer_id also? + Header (7 bytes): + [0] u32 protocol_id + [4] u16 sender_peer_id + [6] u8 channel +sender_peer_id: + Unique to each peer. + value 0 is reserved for making new connections + value 1 is reserved for server +channel: + The lower the number, the higher the priority is. + Only channels 0, 1 and 2 exist. +*/ +#define BASE_HEADER_SIZE 7 +#define PEER_ID_NEW 0 +#define PEER_ID_SERVER 1 +#define CHANNEL_COUNT 3 +/* +Packet types: + +CONTROL: This is a packet used by the protocol. +- When this is processed, nothing is handed to the user. + Header (2 byte): + [0] u8 type + [1] u8 controltype +controltype and data description: + CONTROLTYPE_ACK + [2] u16 seqnum + CONTROLTYPE_SET_PEER_ID + [2] u16 peer_id_new + CONTROLTYPE_PING + - This can be sent in a reliable packet to get a reply +*/ +#define TYPE_CONTROL 0 +#define CONTROLTYPE_ACK 0 +#define CONTROLTYPE_SET_PEER_ID 1 +#define CONTROLTYPE_PING 2 +/* +ORIGINAL: This is a plain packet with no control and no error +checking at all. +- When this is processed, it is directly handed to the user. + Header (1 byte): + [0] u8 type +*/ +#define TYPE_ORIGINAL 1 +#define ORIGINAL_HEADER_SIZE 1 +/* +SPLIT: These are sequences of packets forming one bigger piece of +data. +- When processed and all the packet_nums 0...packet_count-1 are + present (this should be buffered), the resulting data shall be + directly handed to the user. +- If the data fails to come up in a reasonable time, the buffer shall + be silently discarded. +- These can be sent as-is or atop of a RELIABLE packet stream. + Header (7 bytes): + [0] u8 type + [1] u16 seqnum + [3] u16 chunk_count + [5] u16 chunk_num +*/ +#define TYPE_SPLIT 2 +/* +RELIABLE: Delivery of all RELIABLE packets shall be forced by ACKs, +and they shall be delivered in the same order as sent. This is done +with a buffer in the receiving and transmitting end. +- When this is processed, the contents of each packet is recursively + processed as packets. + Header (3 bytes): + [0] u8 type + [1] u16 seqnum + +*/ +#define TYPE_RELIABLE 3 +#define RELIABLE_HEADER_SIZE 3 +#define SEQNUM_INITIAL 0x10 + +#define RESEND_TIMEOUT_MIN 0.2 +#define RESEND_TIMEOUT_MAX 5.0 + +/* + TODO: FIXME: + - Move all and especially thread-sensitive methods to .cpp file + - Think up a better way of handling unreliable split packets + (currently all chunks are wasted if one doesn't arrive) + - Fix reliable packets and use an own channel for these? +*/ + +/* + A buffer which stores reliable packets and sorts them internally + for fast access to the smallest one. +*/ + +typedef core::list::Iterator RPBSearchResult; + +class ReliablePacketBuffer +{ +public: + + void print(); + bool empty(); + u32 size(); + RPBSearchResult findPacket(u16 seqnum); + RPBSearchResult notFound(); + u16 getFirstSeqnum(); + BufferedPacket popFirst(); + BufferedPacket popSeqnum(u16 seqnum); + void insert(BufferedPacket &p); + void incrementTimeouts(float dtime); + void resetTimedOuts(float timeout); + core::list getTimedOuts(float timeout); + +private: + core::list m_list; +}; + +/* + A buffer for reconstructing split packets +*/ + +class IncomingSplitBuffer +{ +public: + ~IncomingSplitBuffer(); + /* + This will throw a GotSplitPacketException when a full + split packet is constructed. + */ + void insert(BufferedPacket &p, bool reliable); + + void removeUnreliableTimedOuts(float dtime, float timeout); + +private: + // Key is seqnum + core::map m_buf; +}; + +class Connection; + +struct Channel +{ + Channel(); + ~Channel(); + /* + Processes a packet with the basic header stripped out. + Parameters: + packetdata: Data in packet (with no base headers) + con: The connection to which the channel is associated + (used for sending back stuff (ACKs)) + peer_id: peer id of the sender of the packet in question + channelnum: channel on which the packet was sent + reliable: true if recursing into a reliable packet + */ + SharedBuffer ProcessPacket( + SharedBuffer packetdata, + Connection *con, + u16 peer_id, + u8 channelnum, + bool reliable=false); + + // Returns next data from a buffer if possible + // throws a NoIncomingDataException if no data is available + // If found, sets peer_id + SharedBuffer CheckIncomingBuffers(Connection *con, + u16 &peer_id); + + u16 next_outgoing_seqnum; + u16 next_incoming_seqnum; + u16 next_outgoing_split_seqnum; + + // This is for buffering the incoming packets that are coming in + // the wrong order + ReliablePacketBuffer incoming_reliables; + // This is for buffering the sent packets so that the sender can + // re-send them if no ACK is received + ReliablePacketBuffer outgoing_reliables; + + IncomingSplitBuffer incoming_splits; +}; + +struct Peer +{ + Peer(); + ~Peer(); + + Channel channels[CHANNEL_COUNT]; + + // Address of the peer + Address address; + // Unique id of the peer + u16 id; + // Seconds from last receive + float timeout_counter; + // Ping timer + float ping_timer; + // This is changed dynamically + float resend_timeout; + // Updated when an ACK is received + float avg_rtt; + // This is set to true when the peer has actually sent something + // with the id we have given to it + bool has_sent_with_id; +}; + +class IndentationRaiser +{ +public: + IndentationRaiser(u16 *indentation) + { + m_indentation = indentation; + (*m_indentation)++; + } + ~IndentationRaiser() + { + (*m_indentation)--; + } +private: + u16 *m_indentation; +}; + +class Connection +{ +public: + Connection(u32 protocol_id, u32 max_packet_size); + ~Connection(); + void setTimeoutMs(int timeout){ m_socket.setTimeoutMs(timeout); } + // Start being a server + void Serve(unsigned short port); + // Connect to a server + void Connect(Address address); + bool Connected(); + + // Sets peer_id + SharedBuffer GetFromBuffers(u16 &peer_id); + + // The peer_id of sender is stored in peer_id + // Return value: I guess this always throws an exception or + // actually gets data + u32 Receive(u16 &peer_id, u8 *data, u32 datasize); + + // These will automatically package the data as an original or split + void SendToAll(u8 channelnum, SharedBuffer data, bool reliable); + void Send(u16 peer_id, u8 channelnum, SharedBuffer data, bool reliable); + // Send data as a packet; it will be wrapped in base header and + // optionally to a reliable packet. + void SendAsPacket(u16 peer_id, u8 channelnum, + SharedBuffer data, bool reliable); + // Sends a raw packet + void RawSend(const BufferedPacket &packet); + + void RunTimeouts(float dtime); + Peer* GetPeer(u16 peer_id); + + void SetPeerID(u16 id){ m_peer_id = id; } + u16 GetPeerID(){ return m_peer_id; } + u32 GetProtocolID(){ return m_protocol_id; } + + // For debug printing + void PrintInfo(); + u16 m_indentation; + +private: + u32 m_protocol_id; + core::map m_peers; + u16 m_peer_id; + bool m_waiting_new_peer_id; + u32 m_max_packet_size; + UDPSocket m_socket; +}; + +} // namespace + +#endif + diff --git a/src/environment.cpp b/src/environment.cpp new file mode 100644 index 0000000..bc28195 --- /dev/null +++ b/src/environment.cpp @@ -0,0 +1,146 @@ +#include "environment.h" + +Environment::Environment(Map *map, std::ostream &dout): + m_dout(dout) +{ + m_map = map; +} + +Environment::~Environment() +{ + // Deallocate players + for(core::list::Iterator i = m_players.begin(); + i != m_players.end(); i++) + { + delete (*i); + } +} + +void Environment::step(float dtime) +{ + // Increment timeout of players + // TODO: Must reset the timeout somewhere, too. + for(core::list::Iterator i = m_players.begin(); + i != m_players.end(); i++) + { + Player *player = *i; + player->timeout_counter += dtime; + } + // Remove timed-out players +removed: + for(core::list::Iterator i = m_players.begin(); + i != m_players.end(); i++) + { + Player *player = *i; + + // Don't remove local player + if(player->isLocal()) + continue; + + // 5 seconds is fine, the player will spawn again with no + // problems anyway + if(player->timeout_counter > 5.0) + { + m_dout<<"Environment: Removing timed-out player " + <peer_id<::Iterator i = m_players.begin(); + i != m_players.end(); i++) + { + f32 speed = (*i)->speed.getLength(); + if(speed > maximum_player_speed) + maximum_player_speed = speed; + } + + // Maximum time increment (for collision detection etc) + // Allow 0.1 blocks per increment + // time = distance / speed + f32 dtime_max_increment = 0.1*BS / maximum_player_speed; + // Maximum time increment is 10ms or lower + if(dtime_max_increment > 0.01) + dtime_max_increment = 0.01; + + /* + Stuff that has a maximum time increment + */ + // Don't allow overly too much dtime + if(dtime > 0.5) + dtime = 0.5; + do + { + f32 dtime_part; + if(dtime > dtime_max_increment) + dtime_part = dtime_max_increment; + else + dtime_part = dtime; + dtime -= dtime_part; + + /* + Move players + */ + for(core::list::Iterator i = m_players.begin(); + i != m_players.end(); i++) + { + Player *player = *i; + player->speed.Y -= 9.81 * BS * dtime_part * 2; + player->move(dtime_part, *m_map); + } + } + while(dtime > 0.001); + +} + +Map & Environment::getMap() +{ + return *m_map; +} + +void Environment::addPlayer(Player *player) +{ + //Check that only one local player exists and peer_ids are unique + assert(player->isLocal() == false || getLocalPlayer() == NULL); + assert(getPlayer(player->peer_id) == NULL); + m_players.push_back(player); +} + +void Environment::removePlayer(Player *player) +{ + //TODO + throw; +} + +Player * Environment::getLocalPlayer() +{ + for(core::list::Iterator i = m_players.begin(); + i != m_players.end(); i++) + { + Player *player = *i; + if(player->isLocal()) + return player; + } + return NULL; +} + +Player * Environment::getPlayer(u16 peer_id) +{ + for(core::list::Iterator i = m_players.begin(); + i != m_players.end(); i++) + { + Player *player = *i; + if(player->peer_id == peer_id) + return player; + } + return NULL; +} + +core::list Environment::getPlayers() +{ + return m_players; +} + diff --git a/src/environment.h b/src/environment.h new file mode 100644 index 0000000..1f8b572 --- /dev/null +++ b/src/environment.h @@ -0,0 +1,48 @@ +#ifndef ENVIRONMENT_HEADER +#define ENVIRONMENT_HEADER + +/* + This class is the game's environment. + It contains: + - The map + - Players + - Other objects + - The current time in the game, etc. +*/ + +#include +#include "common_irrlicht.h" +#include "player.h" +#include "map.h" +#include + +class Environment +{ +public: + // Environment will delete the map passed to the constructor + Environment(Map *map, std::ostream &dout); + ~Environment(); + /* + This can do anything to the environment, such as removing + timed-out players. + */ + void step(f32 dtime); + + Map & getMap(); + /* + Environment deallocates players after use. + */ + void addPlayer(Player *player); + void removePlayer(Player *player); + Player * getLocalPlayer(); + Player * getPlayer(u16 peer_id); + core::list getPlayers(); +private: + Map *m_map; + core::list m_players; + // Debug output goes here + std::ostream &m_dout; +}; + +#endif + diff --git a/src/exceptions.h b/src/exceptions.h new file mode 100644 index 0000000..8c2c0c0 --- /dev/null +++ b/src/exceptions.h @@ -0,0 +1,73 @@ +#ifndef EXCEPTIONS_HEADER +#define EXCEPTIONS_HEADER + +#include + +class BaseException : public std::exception +{ +public: + BaseException(const char *s) + { + m_s = s; + } + virtual const char * what() const throw() + { + return m_s; + } + const char *m_s; +}; + +class AsyncQueuedException : public BaseException +{ +public: + AsyncQueuedException(const char *s): + BaseException(s) + {} +}; + +class NotImplementedException : public BaseException +{ +public: + NotImplementedException(const char *s): + BaseException(s) + {} +}; + +class AlreadyExistsException : public BaseException +{ +public: + AlreadyExistsException(const char *s): + BaseException(s) + {} +}; + +/* + Some "old-style" interrupts: +*/ + +class InvalidPositionException : public std::exception +{ + virtual const char * what() const throw() + { + return "Somebody tried to get/set something in a nonexistent position."; + } +}; + +class TargetInexistentException : public std::exception +{ + virtual const char * what() const throw() + { + return "Somebody tried to refer to something that doesn't exist."; + } +}; + +class NullPointerException : public std::exception +{ + virtual const char * what() const throw() + { + return "NullPointerException"; + } +}; + +#endif + diff --git a/src/heightmap.cpp b/src/heightmap.cpp new file mode 100644 index 0000000..3fca5f1 --- /dev/null +++ b/src/heightmap.cpp @@ -0,0 +1,437 @@ +/* +(c) 2010 Perttu Ahola +*/ + +#include "heightmap.h" + +bool g_heightmap_debugprint = false; + +f32 FixedHeightmap::avgNeighbours(v2s16 p, s16 d) +{ + //printf("avgNeighbours((%i,%i), %i): ", p.X, p.Y, d); + + v2s16 dirs[4] = { + v2s16(1,0), + v2s16(0,1), + v2s16(-1,0), + v2s16(0,-1) + }; + f32 sum = 0.0; + f32 count = 0.0; + for(u16 i=0; i<4; i++){ + v2s16 p2 = p + dirs[i] * d; + f32 n = getGroundHeightParent(p2); + if(n < GROUNDHEIGHT_VALID_MINVALUE) + continue; + sum += n; + count += 1.0; + //printf("(%i,%i)=%f ", p2.X, p2.Y, n); + } + //printf("\n"); + assert(count > 0.001); + return sum / count; +} + +f32 FixedHeightmap::avgDiagNeighbours(v2s16 p, s16 d) +{ + /*printf("avgDiagNeighbours((%i,%i), %i): ", + p.X, p.Y, d);*/ + + v2s16 dirs[4] = { + v2s16(1,1), + v2s16(-1,-1), + v2s16(-1,1), + v2s16(1,-1) + }; + f32 sum = 0.0; + f32 count = 0.0; + for(u16 i=0; i<4; i++){ + v2s16 p2 = p + dirs[i] * d; + f32 n = getGroundHeightParent(p2); + if(n < GROUNDHEIGHT_VALID_MINVALUE) + continue; + sum += n; + count += 1.0; + //printf("(%i,%i)=%f ", p2.X, p2.Y, n); + } + //printf("\n"); + assert(count > 0.001); + return sum / count; +} + +/* + Adds a point to transform into a diamond pattern + a = 2, 4, 8, 16, ... +*/ +void FixedHeightmap::makeDiamond(v2s16 center, s16 a, f32 randmax) +{ + f32 n = avgDiagNeighbours(center, a/2); + // Add (-1.0...1.0) * randmax + n += ((float)rand() / (float)(RAND_MAX/2) - 1.0)*randmax; + setGroundHeightParent(center, n); +} + +/* + Adds points to transform into a diamond pattern + a = 2, 4, 8, 16, ... +*/ +void FixedHeightmap::makeDiamonds(s16 a, f32 randmax) +{ + s16 count_y = (H-1) / a; + s16 count_x = (W-1) / a; + for(s32 yi=0; yi= count_y - 1) + break; + // odd rows + count_x = (W-1) / a + 1; + for(s32 xi=0; xi= 2) + { + makeDiamonds(a, randmax); + if(HEIGHTMAP_DEBUGPRINT){ + printf("MakeDiamonds a=%i result:\n", a); + print(); + } + makeSquares(a, randmax); + if(HEIGHTMAP_DEBUGPRINT){ + printf("MakeSquares a=%i result:\n", a); + print(); + } + a /= 2; + randmax *= randfactor; + } +} + +void UnlimitedHeightmap::print() +{ + s16 minx = 10000; + s16 miny = 10000; + s16 maxx = -10000; + s16 maxy = -10000; + core::map::Iterator i; + i = m_heightmaps.getIterator(); + if(i.atEnd()){ + printf("UnlimitedHeightmap::print(): empty.\n"); + return; + } + for(; i.atEnd() == false; i++) + { + v2s16 p = i.getNode()->getValue()->getPosOnMaster(); + if(p.X < minx) minx = p.X; + if(p.Y < miny) miny = p.Y; + if(p.X > maxx) maxx = p.X; + if(p.Y > maxy) maxy = p.Y; + } + minx = minx * m_blocksize; + miny = miny * m_blocksize; + maxx = (maxx+1) * m_blocksize; + maxy = (maxy+1) * m_blocksize; + printf("UnlimitedHeightmap::print(): from (%i,%i) to (%i,%i)\n", + minx, miny, maxx, maxy); + for(s32 y=miny; y<=maxy; y++){ + for(s32 x=minx; x<=maxx; x++){ + f32 n = getGroundHeight(v2s16(x,y), false); + if(n < GROUNDHEIGHT_VALID_MINVALUE) + printf(" - "); + else + printf("% -5.1f ", getGroundHeight(v2s16(x,y), false)); + } + printf("\n"); + } +} + +FixedHeightmap * UnlimitedHeightmap::getHeightmap(v2s16 p, bool generate) +{ + core::map::Node *n = m_heightmaps.find(p); + + if(n != NULL) + { + return n->getValue(); + } + + /*std::cout<<"UnlimitedHeightmap::getHeightmap((" + <generateContinued(m_randmax, m_randfactor, corners); + + return heightmap; +} + +f32 UnlimitedHeightmap::getGroundHeight(v2s16 p, bool generate) +{ + v2s16 heightmappos = getNodeHeightmapPos(p); + v2s16 relpos = p - heightmappos*m_blocksize; + try{ + FixedHeightmap * href = getHeightmap(heightmappos, generate); + f32 h = href->getGroundHeight(relpos); + if(h > GROUNDHEIGHT_VALID_MINVALUE) + return h; + } + catch(InvalidPositionException){} + + /* + OK, wasn't there. + + Mercilessly try to get it somewhere. + */ +#if 1 + if(relpos.X == 0){ + try{ + FixedHeightmap * href = getHeightmap( + heightmappos-v2s16(1,0), false); + f32 h = href->getGroundHeight(v2s16(m_blocksize, relpos.Y)); + if(h > GROUNDHEIGHT_VALID_MINVALUE) + return h; + } + catch(InvalidPositionException){} + } + if(relpos.Y == 0){ + try{ + FixedHeightmap * href = getHeightmap( + heightmappos-v2s16(0,1), false); + f32 h = href->getGroundHeight(v2s16(relpos.X, m_blocksize)); + if(h > GROUNDHEIGHT_VALID_MINVALUE) + return h; + } + catch(InvalidPositionException){} + } + if(relpos.X == 0 && relpos.Y == 0){ + try{ + FixedHeightmap * href = getHeightmap( + heightmappos-v2s16(1,1), false); + f32 h = href->getGroundHeight(v2s16(m_blocksize, m_blocksize)); + if(h > GROUNDHEIGHT_VALID_MINVALUE) + return h; + } + catch(InvalidPositionException){} + } +#endif + return GROUNDHEIGHT_NOTFOUND_SETVALUE; +} + +void UnlimitedHeightmap::setGroundHeight(v2s16 p, f32 y, bool generate) +{ + v2s16 heightmappos = getNodeHeightmapPos(p); + v2s16 relpos = p - heightmappos*m_blocksize; + /*std::cout<<"UnlimitedHeightmap::setGroundHeight((" + <setGroundHeight(relpos, y); + }catch(InvalidPositionException){} + // Update in neighbour heightmap if it's at border + if(relpos.X == 0){ + try{ + FixedHeightmap * href = getHeightmap( + heightmappos-v2s16(1,0), generate); + href->setGroundHeight(v2s16(m_blocksize, relpos.Y), y); + }catch(InvalidPositionException){} + } + if(relpos.Y == 0){ + try{ + FixedHeightmap * href = getHeightmap( + heightmappos-v2s16(0,1), generate); + href->setGroundHeight(v2s16(relpos.X, m_blocksize), y); + }catch(InvalidPositionException){} + } + if(relpos.X == m_blocksize && relpos.Y == m_blocksize){ + try{ + FixedHeightmap * href = getHeightmap( + heightmappos-v2s16(1,1), generate); + href->setGroundHeight(v2s16(m_blocksize, m_blocksize), y); + }catch(InvalidPositionException){} + } +} + + +void FixedHeightmap::generateContinued(f32 randmax, f32 randfactor, + f32 *corners) +{ + if(HEIGHTMAP_DEBUGPRINT){ + std::cout<<"FixedHeightmap("<getGroundHeight(npos, false); + //std::cout<<"h="< GROUNDHEIGHT_VALID_MINVALUE) + continue; + setGroundHeight(dirs[i] * a, corners[i]); + } + + if(HEIGHTMAP_DEBUGPRINT){ + std::cout<<"corners filled:"< +*/ + +#ifndef HEIGHTMAP_HEADER +#define HEIGHTMAP_HEADER + +#include +#include +#include + +#include "common_irrlicht.h" +#include "exceptions.h" + +#define GROUNDHEIGHT_NOTFOUND_SETVALUE (-10e6) +#define GROUNDHEIGHT_VALID_MINVALUE ( -9e6) + +extern bool g_heightmap_debugprint; +#define HEIGHTMAP_DEBUGPRINT g_heightmap_debugprint + +class Heightmappish +{ +public: + virtual f32 getGroundHeight(v2s16 p, bool generate=true) + { + printf("Heightmappish::getGroundHeight() stub called\n"); + assert(0); + return 0.0; + } + virtual void setGroundHeight(v2s16 p, f32 y, bool generate=true) + { + printf("Heightmappish::setGroundHeight() stub called\n"); + assert(0); + } + v2f32 getSlope(v2s16 p) + { + f32 y0 = getGroundHeight(p, false); + + v2s16 dirs[] = { + v2s16(1,0), + v2s16(0,1), + }; + + v2f32 fdirs[] = { + v2f32(1,0), + v2f32(0,1), + }; + + v2f32 slopevector(0.0, 0.0); + + for(u16 i=0; i<2; i++){ + f32 y1 = 0.0; + f32 y2 = 0.0; + f32 count = 0.0; + + v2s16 p1 = p - dirs[i]; + y1 = getGroundHeight(p1, false); + if(y1 > GROUNDHEIGHT_VALID_MINVALUE){ + y1 -= y0; + count += 1.0; + } + else + y1 = 0; + + v2s16 p2 = p + dirs[i]; + y2 = getGroundHeight(p2, false); + if(y2 > GROUNDHEIGHT_VALID_MINVALUE){ + y2 -= y0; + count += 1.0; + } + else + y2 = 0; + + if(count < 0.001) + return v2f32(0.0, 0.0); + + /* + If y2 is higher than y1, slope is positive + */ + f32 slope = (y2 - y1)/count; + + slopevector += fdirs[i] * slope; + } + + return slopevector; + } + +}; + +class Heightmap : public Heightmappish /*, public ReferenceCounted*/ +{ +}; + +class WrapperHeightmap : public Heightmap +{ + Heightmappish *m_target; +public: + + WrapperHeightmap(Heightmappish *target): + m_target(target) + { + if(target == NULL) + throw NullPointerException(); + } + + f32 getGroundHeight(v2s16 p, bool generate=true) + { + return m_target->getGroundHeight(p, generate); + } + void setGroundHeight(v2s16 p, f32 y, bool generate=true) + { + m_target->setGroundHeight(p, y, generate); + } +}; + +class DummyHeightmap : public Heightmap +{ +public: + f32 m_value; + + DummyHeightmap(f32 value=0.0) + { + m_value = value; + } + + f32 getGroundHeight(v2s16 p, bool generate=true) + { + return m_value; + } + void setGroundHeight(v2s16 p, f32 y, bool generate=true) + { + std::cout<<"DummyHeightmap::getGroundHeight()"<= W || p.Y < 0 || p.Y >= H); + } + + bool atborder(v2s16 p) + { + if(overborder(p)) + return false; + return (p.X == 0 || p.X == W-1 || p.Y == 0 || p.Y == H-1); + } + + void setGroundHeight(v2s16 p, f32 y) + { + /*std::cout<<"FixedHeightmap::setGroundHeight((" + <setGroundHeight(nodepos_master, y, false);*/ + + if(overborder(p) || atborder(p)) + { + try{ + // Position on master + v2s16 blockpos_nodes = m_pos_on_master * m_blocksize; + v2s16 nodepos_master = blockpos_nodes + p; + m_master->setGroundHeight(nodepos_master, y, false); + } + catch(InvalidPositionException &e) + { + } + } + + if(overborder(p)) + return; + + setGroundHeight(p, y); + } + + f32 getGroundHeight(v2s16 p, bool generate=false) + { + if(overborder(p)) + return GROUNDHEIGHT_NOTFOUND_SETVALUE; + return m_data[p.Y*W + p.X]; + } + + f32 getGroundHeightParent(v2s16 p) + { + /*v2s16 blockpos_nodes = m_pos_on_master * m_blocksize; + return m_master->getGroundHeight(blockpos_nodes + p, false);*/ + + if(overborder(p) == false){ + f32 h = getGroundHeight(p); + if(h > GROUNDHEIGHT_VALID_MINVALUE) + return h; + } + + // Position on master + v2s16 blockpos_nodes = m_pos_on_master * m_blocksize; + f32 h = m_master->getGroundHeight(blockpos_nodes + p, false); + return h; + } + + f32 avgNeighbours(v2s16 p, s16 d); + + f32 avgDiagNeighbours(v2s16 p, s16 d); + + /* + Adds a point to transform into a diamond pattern + a = 2, 4, 8, 16, ... + */ + void makeDiamond(v2s16 center, s16 a, f32 randmax); + + /* + Adds points to transform into a diamond pattern + a = 2, 4, 8, 16, ... + */ + void makeDiamonds(s16 a, f32 randmax); + + /* + Adds a point to transform into a square pattern + a = 2, 4, 8, 16, ... + */ + void makeSquare(v2s16 center, s16 a, f32 randmax); + + /* + Adds points to transform into a square pattern + a = 2, 4, 8, 16, ... + */ + void makeSquares(s16 a, f32 randmax); + + void DiamondSquare(f32 randmax, f32 randfactor); + + /* + corners: [i]=XY: [0]=00, [1]=10, [2]=11, [3]=10 + */ + void generateContinued(f32 randmax, f32 randfactor, f32 *corners); +}; + +class OneChildHeightmap : public Heightmap +{ + s16 m_blocksize; + +public: + + FixedHeightmap m_child; + + OneChildHeightmap(s16 blocksize): + m_blocksize(blocksize), + m_child(this, v2s16(0,0), blocksize) + { + } + + f32 getGroundHeight(v2s16 p, bool generate=true) + { + if(p.X < 0 || p.X > m_blocksize + || p.Y < 0 || p.Y > m_blocksize) + return GROUNDHEIGHT_NOTFOUND_SETVALUE; + return m_child.getGroundHeight(p); + } + void setGroundHeight(v2s16 p, f32 y, bool generate=true) + { + //std::cout<<"OneChildHeightmap::setGroundHeight()"< m_blocksize + || p.Y < 0 || p.Y > m_blocksize) + throw InvalidPositionException(); + m_child.setGroundHeight(p, y); + } +}; + +/* + TODO + This is a dynamic container of an arbitrary number of heightmaps + at arbitrary positions. + + It is able to redirect queries to the corresponding heightmaps and + it generates new heightmaps on-the-fly according to the relevant + parameters. + + It doesn't have a master heightmap because it is meant to be used + as such itself. + + Child heightmaps are spaced at m_blocksize distances, and are of + size (m_blocksize+1)*(m_blocksize+1) + + TODO: Dynamic unloading and loading of stuff to/from disk + + This is used as the master heightmap of a Map object. +*/ +class UnlimitedHeightmap: public Heightmap +{ +private: + + core::map m_heightmaps; + s16 m_blocksize; + + f32 m_randmax; + f32 m_randfactor; + f32 m_basevalue; + +public: + + UnlimitedHeightmap(s16 blocksize, f32 randmax, f32 randfactor, + f32 basevalue=0.0): + m_blocksize(blocksize), + m_randmax(randmax), + m_randfactor(randfactor), + m_basevalue(basevalue) + { + } + + ~UnlimitedHeightmap() + { + core::map::Iterator i; + i = m_heightmaps.getIterator(); + for(; i.atEnd() == false; i++) + { + delete i.getNode()->getValue(); + } + } + + void setParams(f32 randmax, f32 randfactor) + { + m_randmax = randmax; + m_randfactor = randfactor; + } + + void print(); + + v2s16 getNodeHeightmapPos(v2s16 p) + { + return v2s16( + (p.X>=0 ? p.X : p.X-m_blocksize+1) / m_blocksize, + (p.Y>=0 ? p.Y : p.Y-m_blocksize+1) / m_blocksize); + } + + FixedHeightmap * getHeightmap(v2s16 p, bool generate=true); + + f32 getGroundHeight(v2s16 p, bool generate=true); + void setGroundHeight(v2s16 p, f32 y, bool generate=true); +}; + +#endif + diff --git a/src/light.h b/src/light.h new file mode 100644 index 0000000..fe641db --- /dev/null +++ b/src/light.h @@ -0,0 +1,38 @@ +#ifndef LIGHT_HEADER +#define LIGHT_HEADER + +/* +RULES OF LIGHT: + +light = 1.0 (0.999-1.001) is SUNLIGHT. It goes downwards infinitely from +infinite highness and stops when it first hits a non-transparent node. +The lighting of the node it hits is 0. + +Other LIGHT SOURCES have a light value of UNDER 0.999. + +Light diminishes at a constant factor between nodes. + +TODO: Should there be a common container to be parent of map, sector and block? + +*/ + +#define LIGHT_MAX 1.0 +#define LIGHT_MIN 0.0 +// If lighting value is under this, it can be assumed that +// there is no light +#define NO_LIGHT_MAX 0.03 + +//#define LIGHT_DIMINISH_FACTOR 0.75 +#define LIGHT_DIMINISH_FACTOR 0.8 + +/* + When something changes lighting of a node, stuff around it is + updated inside this radius. +*/ +//#define LIGHTING_RADIUS 10 +#define LIGHTING_RADIUS 15 + +typedef f32 light_t; + +#endif + diff --git a/src/loadstatus.h b/src/loadstatus.h new file mode 100644 index 0000000..a5fb6b3 --- /dev/null +++ b/src/loadstatus.h @@ -0,0 +1,144 @@ +#ifndef LOADSTATUS_HEADER +#define LOADSTATUS_HEADER + +class LoadStatus +{ + bool ready; + JMutex ready_mutex; + + u32 done; + JMutex done_mutex; + + u32 todo; + JMutex todo_mutex; + + wchar_t *text; + JMutex text_mutex; + +public: + + LoadStatus(bool a_ready=false, u32 a_done=0, u32 a_todo=0) + { + ready = a_ready; + done = a_done; + todo = a_todo; + text = NULL; + ready_mutex.Init(); + done_mutex.Init(); + todo_mutex.Init(); + text_mutex.Init(); + } + + void setReady(bool a_ready) + { + ready_mutex.Lock(); + ready = a_ready; + ready_mutex.Unlock(); + } + + bool getReady(void) + { + ready_mutex.Lock(); + bool a_ready = ready; + ready_mutex.Unlock(); + return a_ready; + } + + void setDone(u32 a_done) + { + done_mutex.Lock(); + done = a_done; + done_mutex.Unlock(); + } + + u32 getDone(void) + { + done_mutex.Lock(); + u32 a_done = done; + done_mutex.Unlock(); + return a_done; + } + + void setTodo(u32 a_todo) + { + todo_mutex.Lock(); + todo = a_todo; + todo_mutex.Unlock(); + } + + u32 getTodo(void) + { + todo_mutex.Lock(); + u32 a_todo = todo; + todo_mutex.Unlock(); + return a_todo; + } + + /* + Copies the text if not NULL, + If NULL; sets text to NULL. + */ + void setText(const wchar_t *a_text) + { + text_mutex.Lock(); + if(text != NULL) + free(text); + if(a_text == NULL){ + text = NULL; + text_mutex.Unlock(); + return; + } + u32 len = wcslen(a_text); + text = (wchar_t*)malloc(sizeof(wchar_t) * (len+1)); + if(text == NULL) throw; + swprintf(text, len+1, L"%ls", a_text); + text_mutex.Unlock(); + } + + /* + Return value must be free'd + Return value can be NULL + */ + wchar_t * getText() + { + text_mutex.Lock(); + if(text == NULL){ + text_mutex.Unlock(); + return NULL; + } + u32 len = wcslen(text); + wchar_t *b_text = (wchar_t*)malloc(sizeof(wchar_t) * (len+1)); + if(b_text == NULL) throw; + swprintf(b_text, len+1, L"%ls", text); + text_mutex.Unlock(); + return b_text; + } + + /* + Return value must be free'd + */ + wchar_t * getNiceText() + { + const wchar_t *defaulttext = L"Loading"; + wchar_t *t = getText(); + u32 maxlen = 20; // " (%i/%i)" + if(t != NULL) + maxlen += wcslen(t); + else + maxlen += wcslen(defaulttext); + wchar_t *b_text = (wchar_t*)malloc(sizeof(wchar_t) * (maxlen+1)); + if(b_text == NULL) throw; + if(t != NULL) + swprintf(b_text, maxlen+1, L"%ls (%i/%i)", + t, getDone(), getTodo()); + else + swprintf(b_text, maxlen+1, L"%ls (%i/%i)", + defaulttext, getDone(), getTodo()); + if(t != NULL) + free(t); + return b_text; + } +}; + +#endif + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6ca6a73 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,976 @@ +/* +(c) 2010 Perttu Ahola + +Minetest + +TODO: Check for obsolete todos +TODO: Storing map on disk, preferably dynamically +TODO: struct settings, with a mutex and get/set functions +TODO: Implement torches and similar light sources (halfway done) +TODO: A menu +TODO: A cache class that can be used with lightNeighbors, + unlightNeighbors and probably many others. Look for + implementation in lightNeighbors +TODO: Proper objects for random stuff in this file, like + g_selected_material +TODO: See if changing to 32-bit position variables doesn't raise + memory consumption a lot. +Now: +TODO: Have to implement mutexes to MapSectors; otherwise initial +lighting might fail +TODO: Adding more heightmap points to MapSectors + +Network protocol: +- Client map data is only updated from the server's, + EXCEPT FOR lighting. + +Actions: + +- User places block +-> Client sends PLACED_BLOCK(pos, node) +-> Server validates and sends MAP_SINGLE_CHANGE(pos, node) +-> Client applies change and recalculates lighting and face cache + +- User starts digging +-> Client sends START_DIGGING(pos) +-> Server starts timer +- if user stops digging: + -> Client sends STOP_DIGGING + -> Server stops timer +- if user continues: + -> Server waits timer + -> Server sends MAP_SINGLE_CHANGE(pos, node) + -> Client applies change and recalculates lighting and face cache + +*/ + +/* + Setting this to 1 enables a special camera mode that forces + the renderers to think that the camera statically points from + the starting place to a static direction. + + This allows one to move around with the player and see what + is actually drawn behind solid things etc. +*/ +#define FIELD_OF_VIEW_TEST 0 + +// Enable unit tests +#define ENABLE_TESTS 1 + +#ifdef _MSC_VER +#pragma comment(lib, "Irrlicht.lib") +#pragma comment(lib, "jthread.lib") +// This would get rid of the console window +//#pragma comment(linker, "/subsystem:windows /ENTRY:mainCRTStartup") +#endif + +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN + #include + #define sleep_ms(x) Sleep(x) +#else + #include + #define sleep_ms(x) usleep(x*1000) +#endif + +#include +#include +#include +#include "common_irrlicht.h" +#include "map.h" +#include "player.h" +#include "main.h" +#include "test.h" +#include "environment.h" +#include "server.h" +#include "client.h" +#include + +const char *g_material_filenames[MATERIALS_COUNT] = +{ + "../data/stone.png", + "../data/grass.png", + "../data/water.png", +}; + +#define FPS_MIN 15 +#define FPS_MAX 25 + +#define VIEWING_RANGE_NODES_MIN MAP_BLOCKSIZE +#define VIEWING_RANGE_NODES_MAX 35 + +JMutex g_viewing_range_nodes_mutex; +s16 g_viewing_range_nodes = MAP_BLOCKSIZE; + +/* + Random stuff +*/ +u16 g_selected_material = 0; + +/* + Debug streams + - use these to disable or enable outputs of parts of the program +*/ + +std::ofstream dfile("debug.txt"); +//std::ofstream dfile2("debug2.txt"); + +// Connection +//std::ostream dout_con(std::cout.rdbuf()); +std::ostream dout_con(dfile.rdbuf()); + +// Server +//std::ostream dout_server(std::cout.rdbuf()); +std::ostream dout_server(dfile.rdbuf()); + +// Client +//std::ostream dout_client(std::cout.rdbuf()); +std::ostream dout_client(dfile.rdbuf()); + +/* + TimeTaker +*/ + +class TimeTaker +{ +public: + TimeTaker(const char *name, IrrlichtDevice *dev) + { + m_name = name; + m_dev = dev; + m_time1 = m_dev->getTimer()->getRealTime(); + } + ~TimeTaker() + { + u32 time2 = m_dev->getTimer()->getRealTime(); + u32 dtime = time2 - m_time1; + std::cout< 0){ + counter -= frametime; + return; + } + counter = 5.0; //seconds + + g_viewing_range_nodes_mutex.Lock(); + bool changed = false; + if(frametime > 1.0/FPS_MIN + || g_viewing_range_nodes > VIEWING_RANGE_NODES_MAX){ + if(g_viewing_range_nodes > VIEWING_RANGE_NODES_MIN){ + g_viewing_range_nodes -= MAP_BLOCKSIZE/2; + changed = true; + } + } + else if(frametime < 1.0/FPS_MAX + || g_viewing_range_nodes < VIEWING_RANGE_NODES_MIN){ + if(g_viewing_range_nodes < VIEWING_RANGE_NODES_MAX){ + g_viewing_range_nodes += MAP_BLOCKSIZE/2; + changed = true; + } + } + if(changed){ + std::cout<<"g_viewing_range_nodes = " + <getTimer()->getRealTime(); + tempf = 0.001; + for(u32 i=0; i<10000000; i++){ + temp16 += tempf; + tempf += 0.001; + } + u32 time2 = device->getTimer()->getRealTime(); + u32 fp_conversion_time = time2 - time1; + std::cout<<"Done. "<getTimer()->getRealTime(); + + tempv3f1 = v3f(1,2,3); + tempv3f2 = v3f(4,5,6); + for(u32 i=0; i<40000000; i++){ + tempf += tempv3f1.dotProduct(tempv3f2); + tempv3f2 += v3f(7,8,9); + } + + u32 time2 = device->getTimer()->getRealTime(); + u32 dtime = time2 - time1; + std::cout<<"Done. "<getTimer()->getRealTime(); + + core::map map1; + tempf = -324; + for(s16 y=0; y<500; y++){ + for(s16 x=0; x<500; x++){ + map1.insert(v2s16(x,y), tempf); + tempf += 1; + } + } + for(s16 y=500-1; y>=0; y--){ + for(s16 x=0; x<500; x++){ + tempf = map1[v2s16(x,y)]; + } + } + + u32 time2 = device->getTimer()->getRealTime(); + u32 dtime = time2 - time1; + std::cout<<"Done. "<getTimer()->getRealTime(); + u32 time2 = time1; + + JMutex m; + m.Init(); + u32 n = 0; + u32 i = 0; + do{ + n += 10000; + for(; igetTimer()->getRealTime(); + } + // Do at least 10ms + while(time2 < time1 + 10); + + u32 dtime = time2 - time1; + u32 per_ms = n / dtime; + std::cout<<"Done. "< "<>r0; + if(r0 > res_count || r0 == 0) + r0 = 0; + u16 screenW = resolutions[r0-1][0]; + u16 screenH = resolutions[r0-1][1]; + */ + + // + + video::E_DRIVER_TYPE driverType; + +#ifdef _WIN32 + //driverType = video::EDT_DIRECT3D9; // Doesn't seem to work + driverType = video::EDT_OPENGL; +#else + driverType = video::EDT_OPENGL; +#endif + + IrrlichtDevice *device; + device = createDevice(driverType, + core::dimension2d(screenW, screenH), + 16, false, false, false, &receiver); + + if (device == 0) + return 1; // could not create selected driver. + + /* + Run some speed tests + */ + //SpeedTests(device); + + /* + Continue initialization + */ + + video::IVideoDriver* driver = device->getVideoDriver(); + // These make the textures not to show at all + //driver->setTextureCreationFlag(video::ETCF_ALWAYS_16_BIT); + //driver->setTextureCreationFlag(video::ETCF_OPTIMIZED_FOR_SPEED ); + + scene::ISceneManager* smgr = device->getSceneManager(); + + gui::IGUIEnvironment* guienv = device->getGUIEnvironment(); + gui::IGUISkin* skin = guienv->getSkin(); + gui::IGUIFont* font = guienv->getFont("../data/fontlucida.png"); + if(font) + skin->setFont(font); + //skin->setColor(gui::EGDC_BUTTON_TEXT, video::SColor(255,0,0,0)); + skin->setColor(gui::EGDC_BUTTON_TEXT, video::SColor(255,255,255,255)); + //skin->setColor(gui::EGDC_3D_HIGH_LIGHT, video::SColor(0,0,0,0)); + //skin->setColor(gui::EGDC_3D_SHADOW, video::SColor(0,0,0,0)); + skin->setColor(gui::EGDC_3D_HIGH_LIGHT, video::SColor(255,0,0,0)); + skin->setColor(gui::EGDC_3D_SHADOW, video::SColor(255,0,0,0)); + + const wchar_t *text = L"Loading..."; + core::vector2d center(screenW/2, screenH/2); + core::dimension2d textd = font->getDimension(text); + std::cout<<"Text w="<getTexture(filename)); + } + //materials[i].setFlag(video::EMF_TEXTURE_WRAP, video::ETC_REPEAT); + materials[i].setFlag(video::EMF_BILINEAR_FILTER, false); + //materials[i].setFlag(video::EMF_ANISOTROPIC_FILTER, false); + } + + // Make a scope here for the client so that it gets removed + // before the irrlicht device + { + + std::cout<<"Creating server and client"<start(port); + } + + Client client(smgr, materials); + Address connect_address(0,0,0,0, port); + try{ + connect_address.Resolve(connect_name); + } + catch(ResolveError &e) + { + std::cout<<"Couldn't resolve address"<addCameraSceneNode( + 0, // Camera parent + v3f(BS*100, BS*2, BS*100), // Look from + v3f(BS*100+1, BS*2, BS*100), // Look to + -1 // Camera ID + ); + + if(camera == NULL) + return 1; + + camera->setFOV(FOV_ANGLE); + // Just so big a value that everything rendered is visible + camera->setFarValue(BS*1000); + + f32 camera_yaw = 0; // "right/left" + f32 camera_pitch = 0; // "up/down" + + // Random constants +#define WALK_ACCELERATION (4.0 * BS) +#define WALKSPEED_MAX (4.0 * BS) +//#define WALKSPEED_MAX (20.0 * BS) + f32 walk_acceleration = WALK_ACCELERATION; + f32 walkspeed_max = WALKSPEED_MAX; + + /* + The mouse cursor needs not be visible, so we hide it via the + irr::IrrlichtDevice::ICursorControl. + */ + device->getCursorControl()->setVisible(false); + + gui_loadingtext->remove(); + + gui::IGUIStaticText *guitext = guienv->addStaticText( + L"Minetest-c55", core::rect(5, 5, 5+300, 5+textsize.Y), + false, false); + /* + Main loop + */ + + bool first_loop_after_window_activation = true; + + s32 lastFPS = -1; + + // Time is in milliseconds + u32 lasttime = device->getTimer()->getTime(); + + while(device->run()) + { + /* + Time difference calculation + */ + u32 time = device->getTimer()->getTime(); + f32 dtime; // in seconds + if(time > lasttime) + dtime = (time - lasttime) / 1000.0; + else + dtime = 0; + lasttime = time; + + updateViewingRange(dtime); + + // Collected during the loop and displayed + core::list< core::aabbox3d > hilightboxes; + + /* + Special keys + */ + if(receiver.IsKeyDown(irr::KEY_ESCAPE)) + { + break; + } + + /* + Player speed control + */ + + v3f move_direction = v3f(0,0,1); + move_direction.rotateXZBy(camera_yaw); + + v3f speed = v3f(0,0,0); + if(receiver.IsKeyDown(irr::KEY_KEY_W)) + { + speed += move_direction; + } + if(receiver.IsKeyDown(irr::KEY_KEY_S)) + { + speed -= move_direction; + } + if(receiver.IsKeyDown(irr::KEY_KEY_A)) + { + speed += move_direction.crossProduct(v3f(0,1,0)); + } + if(receiver.IsKeyDown(irr::KEY_KEY_D)) + { + speed += move_direction.crossProduct(v3f(0,-1,0)); + } + if(receiver.IsKeyDown(irr::KEY_SPACE)) + { + if(player->touching_ground){ + //player_speed.Y = 30*BS; + //player.speed.Y = 5*BS; + player->speed.Y = 6.5*BS; + } + } + + // The speed of the player (Y is ignored) + speed = speed.normalize() * walkspeed_max; + + f32 inc = walk_acceleration * BS * dtime; + + if(player->speed.X < speed.X - inc) + player->speed.X += inc; + else if(player->speed.X > speed.X + inc) + player->speed.X -= inc; + else if(player->speed.X < speed.X) + player->speed.X = speed.X; + else if(player->speed.X > speed.X) + player->speed.X = speed.X; + + if(player->speed.Z < speed.Z - inc) + player->speed.Z += inc; + else if(player->speed.Z > speed.Z + inc) + player->speed.Z -= inc; + else if(player->speed.Z < speed.Z) + player->speed.Z = speed.Z; + else if(player->speed.Z > speed.Z) + player->speed.Z = speed.Z; + + /* + Process environment + */ + + { + //TimeTaker("client.step(dtime)", device); + client.step(dtime); + } + + if(server != NULL){ + //TimeTaker("server->step(dtime)", device); + server->step(dtime); + } + + /* + Mouse and camera control + */ + + if(device->isWindowActive()) + { + if(first_loop_after_window_activation){ + first_loop_after_window_activation = false; + } + else{ + s32 dx = device->getCursorControl()->getPosition().X - 320; + s32 dy = device->getCursorControl()->getPosition().Y - 240; + camera_yaw -= dx*0.2; + camera_pitch += dy*0.2; + if(camera_pitch < -89.9) camera_pitch = -89.9; + if(camera_pitch > 89.9) camera_pitch = 89.9; + } + device->getCursorControl()->setPosition(320, 240); + } + else{ + first_loop_after_window_activation = true; + } + + v3f camera_direction = v3f(0,0,1); + camera_direction.rotateYZBy(camera_pitch); + camera_direction.rotateXZBy(camera_yaw); + + v3f camera_position = + player->getPosition() + v3f(0, BS+BS/2, 0); + + camera->setPosition(camera_position); + camera->setTarget(camera_position + camera_direction); + + if(FIELD_OF_VIEW_TEST){ + //client.m_env.getMap().updateCamera(v3f(0,0,0), v3f(0,0,1)); + client.updateCamera(v3f(0,0,0), v3f(0,0,1)); + } + else{ + //client.m_env.getMap().updateCamera(camera_position, camera_direction); + client.updateCamera(camera_position, camera_direction); + } + + /* + Calculate what block is the crosshair pointing to + */ + + //u32 t1 = device->getTimer()->getTime(); + + f32 d = 4; // max. distance + core::line3d shootline(camera_position, + camera_position + camera_direction * BS * (d+1)); + + bool nodefound = false; + v3s16 nodepos; + v3s16 neighbourpos; + core::aabbox3d nodefacebox; + f32 mindistance = BS * 1001; + + v3s16 pos_i = Map::floatToInt(player->getPosition()); + + s16 a = d; + s16 ystart = pos_i.Y + 0 - (camera_direction.Y<0 ? a : 1); + s16 zstart = pos_i.Z - (camera_direction.Z<0 ? a : 1); + s16 xstart = pos_i.X - (camera_direction.X<0 ? a : 1); + s16 yend = pos_i.Y + 1 + (camera_direction.Y>0 ? a : 1); + s16 zend = pos_i.Z + (camera_direction.Z>0 ? a : 1); + s16 xend = pos_i.X + (camera_direction.X>0 ? a : 1); + + for(s16 y = ystart; y <= yend; y++){ + for(s16 z = zstart; z <= zend; z++){ + for(s16 x = xstart; x <= xend; x++) + { + try{ + //if(client.m_env.getMap().getNode(x,y,z).d == MATERIAL_AIR){ + if(client.getNode(v3s16(x,y,z)).d == MATERIAL_AIR){ + continue; + } + }catch(InvalidPositionException &e){ + continue; + } + + v3s16 np(x,y,z); + v3f npf = Map::intToFloat(np); + + f32 d = 0.01; + + v3s16 directions[6] = { + v3s16(0,0,1), // back + v3s16(0,1,0), // top + v3s16(1,0,0), // right + v3s16(0,0,-1), + v3s16(0,-1,0), + v3s16(-1,0,0), + }; + + for(u16 i=0; i<6; i++){ + //{u16 i=3; + v3f dir_f = v3f(directions[i].X, + directions[i].Y, directions[i].Z); + v3f centerpoint = npf + dir_f * BS/2; + f32 distance = + (centerpoint - camera_position).getLength(); + + if(distance < mindistance){ + //std::cout<<"for centerpoint=("< m; + m.buildRotateFromTo(v3f(0,0,1), dir_f); + + // This is the back face + v3f corners[2] = { + v3f(BS/2, BS/2, BS/2), + v3f(-BS/2, -BS/2, BS/2+d) + }; + + for(u16 j=0; j<2; j++){ + m.rotateVect(corners[j]); + corners[j] += npf; + //std::cout<<"box corners["< facebox(corners[0],corners[1]); + core::aabbox3d facebox(corners[0]); + facebox.addInternalPoint(corners[1]); + + if(facebox.intersectsWithLine(shootline)){ + nodefound = true; + nodepos = np; + neighbourpos = np + directions[i]; + mindistance = distance; + nodefacebox = facebox; + } + } + } + }}} + + if(nodefound){ + //std::cout<<"nodefound == true"<setText(positiontext);*/ + } + + hilightboxes.push_back(nodefacebox); + + if(receiver.leftclicked){ + std::cout<<"Removing block (MapNode)"<getTimer()->getRealTime(); + + //client.m_env.getMap().removeNodeAndUpdate(nodepos); + client.removeNode(nodepos); + + u32 time2 = device->getTimer()->getRealTime(); + u32 dtime = time2 - time1; + std::cout<<"Took "<getTimer()->getRealTime(); + + /*f32 light = client.m_env.getMap().getNode(neighbourpos).light; + MapNode n; + n.d = g_selected_material; + client.m_env.getMap().setNode(neighbourpos, n); + client.m_env.getMap().nodeAddedUpdate(neighbourpos, light);*/ + MapNode n; + n.d = g_selected_material; + client.addNode(neighbourpos, n); + + u32 time2 = device->getTimer()->getRealTime(); + u32 dtime = time2 - time1; + std::cout<<"Took "<setText(L""); + } + + receiver.leftclicked = false; + receiver.rightclicked = false; + + /* + Update gui stuff + */ + + static u8 old_selected_material = MATERIAL_AIR; + if(g_selected_material != old_selected_material) + { + old_selected_material = g_selected_material; + wchar_t temptext[50]; + swprintf(temptext, 50, L"Minetest-c55 (F: material=%i)", + g_selected_material); + guitext->setText(temptext); + } + + /* + Drawing begins + */ + + /* + Background color is choosen based on whether the player is + much beyond the initial ground level + */ + /*video::SColor bgcolor; + v3s16 p0 = Map::floatToInt(player->position); + s16 gy = client.m_env.getMap().getGroundHeight(v2s16(p0.X, p0.Z)); + if(p0.Y > gy - MAP_BLOCKSIZE) + bgcolor = video::SColor(255,90,140,200); + else + bgcolor = video::SColor(255,0,0,0);*/ + video::SColor bgcolor = video::SColor(255,90,140,200); + + driver->beginScene(true, true, bgcolor); + + //std::cout<<"smgr->drawAll()"<drawAll(); + + core::vector2d displaycenter(screenW/2,screenH/2); + driver->draw2DLine(displaycenter - core::vector2d(10,0), + displaycenter + core::vector2d(10,0), + video::SColor(255,255,255,255)); + driver->draw2DLine(displaycenter - core::vector2d(0,10), + displaycenter + core::vector2d(0,10), + video::SColor(255,255,255,255)); + + video::SMaterial m; + m.Thickness = 10; + m.Lighting = false; + driver->setMaterial(m); + + for(core::list< core::aabbox3d >::Iterator i=hilightboxes.begin(); + i != hilightboxes.end(); i++){ + driver->draw3DBox(*i, video::SColor(255,0,0,0)); + } + + guienv->drawAll(); + + driver->endScene(); + + /* + Drawing ends + */ + + u16 fps = driver->getFPS(); + + if (lastFPS != fps) + { + core::stringw str = L"Minetest ["; + str += driver->getName(); + str += "] FPS:"; + str += fps; + + device->setWindowCaption(str.c_str()); + lastFPS = fps; + } + + + /*} + else + device->yield();*/ + } + + if(server != NULL) + delete server; + + } // client is deleted at this point + + /* + In the end, delete the Irrlicht device. + */ + device->drop(); + + return 0; +} + +//END diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..04102a3 --- /dev/null +++ b/src/main.h @@ -0,0 +1,26 @@ +/* +(c) 2010 Perttu Ahola +*/ + +#ifndef MAIN_HEADER +#define MAIN_HEADER + +#include + +#define PI 3.14159 + +#define FOV_ANGLE (PI/2.5) + +// Change to struct settings or something +extern s16 g_viewing_range_nodes; +extern JMutex g_viewing_range_nodes_mutex; + +#include + +// Debug streams +extern std::ostream dout_con; +extern std::ostream dout_client; +extern std::ostream dout_server; + +#endif + diff --git a/src/map.cpp b/src/map.cpp new file mode 100644 index 0000000..a4e0a2e --- /dev/null +++ b/src/map.cpp @@ -0,0 +1,1204 @@ +/* +(c) 2010 Perttu Ahola +*/ + +#include "map.h" +//#include "player.h" +#include "main.h" +#include "jmutexautolock.h" +#include "client.h" + +#ifdef _WIN32 + #include + #define sleep_ms(x) Sleep(x) +#else + #include + #define sleep_ms(x) usleep(x*1000) +#endif + +/* +void limitBox(core::aabbox3d & box, core::aabbox3d & limits) +{ + if(box.MinEdge.X < limits.MinEdge.X) + box.MinEdge.X = limits.MinEdge.X; + if(box.MaxEdge.X > limits.MaxEdge.X) + box.MaxEdge.X = limits.MaxEdge.X; + if(box.MinEdge.Y < limits.MinEdge.Y) + box.MinEdge.Y = limits.MinEdge.Y; + if(box.MaxEdge.Y > limits.MaxEdge.Y) + box.MaxEdge.Y = limits.MaxEdge.Y; + if(box.MinEdge.Z < limits.MinEdge.Z) + box.MinEdge.Z = limits.MinEdge.Z; + if(box.MaxEdge.Z > limits.MaxEdge.Z) + box.MaxEdge.Z = limits.MaxEdge.Z; +} +*/ + +void * MapUpdateThread::Thread() +{ + if(map == NULL) + return NULL; + + ThreadStarted(); + + while(getRun()) + { + //std::cout<<"UpdateThread running"<updateChangedVisibleArea(); + + if(did_something == false) + sleep_ms(500); + } + + std::cout<<"UpdateThread stopped"<::Iterator i = m_sectors.getIterator(); + for(; i.atEnd() == false; i++) + { + MapSector *sector = i.getNode()->getValue(); + delete sector; + } +} + +/* + TODO: These mutexes need rethinking. They don't work properly + at the moment. Maybe. Or do they? +*/ + +/*bool Map::sectorExists(v2s16 p) +{ + JMutexAutoLock lock(m_getsector_mutex); + core::map::Node *n = m_sectors.find(p); + return (n != NULL); +}*/ + +MapSector * Map::getSectorNoGenerate(v2s16 p) +{ + //JMutexAutoLock lock(m_getsector_mutex); + + if(m_sector_cache != NULL && p == m_sector_cache_p){ + MapSector * ref(m_sector_cache); + return ref; + } + + core::map::Node *n = m_sectors.find(p); + // If sector doesn't exist, throw an exception + if(n == NULL) + { + /* + TODO: Check if sector is stored on disk + and load dynamically + */ + //sector = NULL; + throw InvalidPositionException(); + } + + MapSector *sector = n->getValue(); + + // Cache the last result + m_sector_cache_p = p; + m_sector_cache = sector; + + //MapSector * ref(sector); + + return sector; +} + +MapSector * Map::getSector(v2s16 p2d) +{ + // Stub virtual method + assert(0); + return getSectorNoGenerate(p2d); +} + +/* + If sector doesn't exist, returns NULL + Otherwise returns what the sector returns + + TODO: Change this to use exceptions? +*/ +MapBlock * Map::getBlockNoCreate(v3s16 p3d) +{ + /*std::cout<<"Map::getBlockNoCreate((" + <mutex); + + MapBlock * blockref = sector->getBlockNoCreate(p3d.Y); + + return blockref; +} + +/* + If sector doesn't exist, generates a sector. + Returns what the sector returns. +*/ +MapBlock * Map::getBlock(v3s16 p3d) +{ + v2s16 p2d(p3d.X, p3d.Z); + //v2s16 sectorpos = getNodeSectorPos(p2d); + MapSector * sref = getSector(p2d); + + /*std::cout<<"Map::GetBlock(): sref->getRefcount()=" + <getRefcount() + <mutex); + + MapBlock * blockref = sector->getBlock(p3d.Y); + + /*std::cout<<"Map::GetBlock(): blockref->getRefcount()=" + <getRefcount() + <mutex.Lock(); + f32 y = sref->getGroundHeight(relpos); + //sref->mutex.Unlock(); + //std::cout<<"[found]"<mutex.Lock(); + sref->setGroundHeight(relpos, y); + //sref->mutex.Unlock(); +} + +/* + TODO: Check the order of changing lighting and recursing in + these functions (choose the faster one) + + TODO: It has to done like this: cache the neighbours that were + changed; then recurse to them. +*/ + +/* + Goes recursively through the neighbours of the node. + + Alters only transparent nodes. + + If the lighting of the neighbour is lower than the lighting of + the node was (before changing it to 0 at the step before), the + lighting of the neighbour is set to 0 and then the same stuff + repeats for the neighbour. + + Some things are made strangely to make it as fast as possible. + + Usage: + core::list light_sources; + core::map modified_blocks; + f32 oldlight = node_at_pos.light; + node_at_pos.light = 0; + unLightNeighbors(pos, oldlight, light_sources, modified_blocks); +*/ +void Map::unLightNeighbors(v3s16 pos, f32 oldlight, + core::list & light_sources, + core::map & modified_blocks) +{ + v3s16 dirs[6] = { + v3s16(0,0,1), // back + v3s16(0,1,0), // top + v3s16(1,0,0), // right + v3s16(0,0,-1), // front + v3s16(0,-1,0), // bottom + v3s16(-1,0,0), // left + }; + + // Loop through 6 neighbors + for(u16 i=0; i<6; i++){ + // Get the position of the neighbor node + v3s16 n2pos = pos + dirs[i]; + + // Get the block where the node is located + v3s16 blockpos = getNodeBlockPos(n2pos); + MapBlock *block; + try{ + block = getBlockNoCreate(blockpos); + } + catch(InvalidPositionException &e) + { + // If block is inexistent, move to next one. + continue; + } + + // Find if block is in cache container + core::map::Node *cacheblocknode; + cacheblocknode = modified_blocks.find(blockpos); + + // If the block is not found in the cache + if(cacheblocknode == NULL) + { + /* + Add block to cache container. It could be a 'set' but + there is no such thing in Irrlicht. + + We can use the pointer as the value of a map just fine, + it gets nicely cached that way, too. + */ + modified_blocks.insert(blockpos, block); + } + + // Calculate relative position in block + v3s16 relpos = n2pos - blockpos * MAP_BLOCKSIZE; + // Get node straight from the block (fast!) + MapNode *n2 = block->getNodePtr(relpos); + + /* + If the neighbor is dimmer than what was specified + as oldlight (the light of the previous node)... + */ + if(n2->light < oldlight - 0.001){ + if(n2->transparent()){ + light_t current_light = n2->light; + n2->light = 0.0; + unLightNeighbors(n2pos, current_light, + light_sources, modified_blocks); + } + } + else{ + light_sources.push_back(n2pos); + } + } +} + +/* + Goes recursively through the neighbours of the node. + + Alters only transparent nodes. + + If the lighting of the neighbour is lower than the calculated + lighting coming from the current node to it, the lighting of + the neighbour is set and the same thing repeats. + + Some things are made strangely to make it as fast as possible. + + Usage: + core::map modified_blocks; + lightNeighbors(pos, node_at_pos.light, modified_blocks); +*/ +/*void Map::lightNeighbors(v3s16 pos, f32 oldlight, + core::map & modified_blocks)*/ +void Map::lightNeighbors(v3s16 pos, + core::map & modified_blocks) +{ + v3s16 dirs[6] = { + v3s16(0,0,1), // back + v3s16(0,1,0), // top + v3s16(1,0,0), // right + v3s16(0,0,-1), // front + v3s16(0,-1,0), // bottom + v3s16(-1,0,0), // left + }; + + core::list neighbour_cache; + + /* + Initialize block cache + (using center node as a starting position) + */ + v3s16 blockpos_last = getNodeBlockPos(pos); + MapBlock *block = NULL; + try{ + block = getBlockNoCreate(blockpos_last); + } + catch(InvalidPositionException &e) + { + return; + } + + // Calculate relative position in block + v3s16 relpos = pos - blockpos_last * MAP_BLOCKSIZE; + // Get node straight from the block (fast!) + MapNode *n = block->getNodePtr(relpos); + + f32 oldlight = n->light; + f32 newlight = oldlight * LIGHT_DIMINISH_FACTOR; + + // Loop through 6 neighbors + for(u16 i=0; i<6; i++){ + // Get the position of the neighbor node + v3s16 n2pos = pos + dirs[i]; + + // Get the block where the node is located + v3s16 blockpos = getNodeBlockPos(n2pos); + + try + { + /* + Only fetch a new block if the block position has changed + */ + if(blockpos != blockpos_last){ + block = getBlockNoCreate(blockpos); + } + blockpos_last = blockpos; + + // Find if block is in cache container + core::map::Node *cacheblocknode; + cacheblocknode = modified_blocks.find(blockpos); + + // If the block is not found in the cache + if(cacheblocknode == NULL) + { + /* + Add block to cache container. It could be a 'set' but + there is no such thing in Irrlicht. + + We can use the pointer as the value of a map just fine, + it gets nicely cached that way, too. + */ + modified_blocks.insert(blockpos, block); + } + + // Calculate relative position in block + v3s16 relpos = n2pos - blockpos * MAP_BLOCKSIZE; + // Get node straight from the block (fast!) + MapNode *n2 = block->getNodePtr(relpos); + + /* + If the neighbor is dimmer than what was specified + as oldlight (the light of the previous node)... + */ + if(n2->light_source() > oldlight / LIGHT_DIMINISH_FACTOR + 0.01) + { + n2->light = n2->light_source(); + neighbour_cache.push_back(n2pos); + } + if(n2->light < newlight - 0.001) + { + if(n2->transparent()) + { + n2->light = newlight; + // Cache and recurse at last step for maximum speed + neighbour_cache.push_back(n2pos); + } + } + } + catch(InvalidPositionException &e) + { + continue; + } + } + + core::list::Iterator j = neighbour_cache.begin(); + for(; j != neighbour_cache.end(); j++){ + lightNeighbors(*j, modified_blocks); + } +} + +v3s16 Map::getBrightestNeighbour(v3s16 p) +{ + v3s16 dirs[6] = { + v3s16(0,0,1), // back + v3s16(0,1,0), // top + v3s16(1,0,0), // right + v3s16(0,0,-1), // front + v3s16(0,-1,0), // bottom + v3s16(-1,0,0), // left + }; + + f32 brightest_light = -1.0; + v3s16 brightest_pos(0,0,0); + + // Loop through 6 neighbors + for(u16 i=0; i<6; i++){ + // Get the position of the neighbor node + v3s16 n2pos = p + dirs[i]; + MapNode n2; + try{ + n2 = getNode(n2pos); + } + catch(InvalidPositionException &e) + { + continue; + } + if(n2.light > brightest_light){ + brightest_light = n2.light; + brightest_pos = n2pos; + } + } + if(brightest_light < -0.001) + throw; + return brightest_pos; +} + +/* + Propagates sunlight down from a node. + Starting point gets sunlight. + + Returns the lowest y value of where the sunlight went. +*/ +s16 Map::propagateSunlight(v3s16 start, + core::map & modified_blocks) +{ + s16 y = start.Y; + for(; ; y--) + { + v3s16 pos(start.X, y, start.Z); + + v3s16 blockpos = getNodeBlockPos(pos); + MapBlock *block; + try{ + block = getBlockNoCreate(blockpos); + } + catch(InvalidPositionException &e) + { + break; + } + + v3s16 relpos = pos - blockpos*MAP_BLOCKSIZE; + MapNode *n = block->getNodePtr(relpos); + + if(n->transparent()) + { + n->light = 1.0; + + modified_blocks.insert(blockpos, block); + } + else{ + break; + } + } + return y + 1; +} + +void Map::updateLighting(core::list< MapBlock*> & a_blocks, + core::map & modified_blocks) +{ + std::cout<<"Map::updateLighting(): " + < temp_blocks = a_blocks; + + /* + Go from the highest blocks to the lowest, propagating sunlight + through them. + */ + + while(temp_blocks.empty() == false){ + // Get block with highest position in Y + + core::list< MapBlock * >::Iterator highest_i = temp_blocks.end(); + v3s16 highest_pos(0,-32768,0); + + core::list< MapBlock * >::Iterator i; + for(i = temp_blocks.begin(); i != temp_blocks.end(); i++) + { + MapBlock *block = *i; + v3s16 pos = block->getPosRelative(); + if(highest_i == temp_blocks.end() || pos.Y > highest_pos.Y){ + highest_i = i; + highest_pos = pos; + } + } + + // Update sunlight in block + MapBlock *block = *highest_i; + block->propagateSunlight(); + + /*std::cout<<"block ("<getPosRelative().X<<"," + <getPosRelative().Y<<"," + <getPosRelative().Z<<") "; + std::cout<<"touches_bottom="<::Iterator i = a_blocks.begin(); + for(; i != a_blocks.end(); i++) + { + MapBlock *block = *i; + v3s16 pos = block->getPosRelative(); + s16 xmin = pos.X; + s16 zmin = pos.Z; + s16 ymin = pos.Y; + s16 xmax = pos.X + MAP_BLOCKSIZE - 1; + s16 zmax = pos.Z + MAP_BLOCKSIZE - 1; + s16 ymax = pos.Y + MAP_BLOCKSIZE - 1; + for(s16 z=zmin; z<=zmax; z++){ + for(s16 x=xmin; x<=xmax; x++){ + for(s16 y=ymax; y>=ymin; y--){ + v3s16 pos(x, y, z); + MapNode n; + try{ + n = getNode(pos); + } + catch(InvalidPositionException &e) + { + break; + } + if(n.light > NO_LIGHT_MAX){ + v3s16 pos(x,y,z); + lightNeighbors(pos, modified_blocks); + } + } + } + } + std::cout<<"X"; + std::cout.flush(); + + //status.setDone(status.getDone()+1); + } + std::cout< light_sources; + core::map modified_blocks; + //MapNode n = getNode(p); + + /* + From this node to nodes underneath: + If lighting is sunlight (1.0), unlight neighbours and + set lighting to 0. + Else discontinue. + */ + + bool node_under_sunlight = true; + + v3s16 toppos = p + v3s16(0,1,0); + + /* + If there is a node at top and it doesn't have sunlight, + there has not been any sunlight going down. + + Otherwise there probably is. + */ + try{ + MapNode topnode = getNode(toppos); + + if(topnode.light < 0.999) + node_under_sunlight = false; + } + catch(InvalidPositionException &e) + { + } + + // Add the block of the added node to modified_blocks + v3s16 blockpos = getNodeBlockPos(p); + MapBlock * blockref = getBlockNoCreate(blockpos); + MapBlock *block = blockref; + assert(block != NULL); + modified_blocks.insert(blockpos, block); + + // Unlight neighbours of node. + // This means setting light of all consequent dimmer nodes + // to 0. + + if(isValidPosition(p) == false) + throw; + + unLightNeighbors(p, lightwas, light_sources, modified_blocks); + + MapNode n = getNode(p); + n.light = 0; + setNode(p, n); + + if(node_under_sunlight) + { + s16 y = p.Y - 1; + for(;; y--){ + std::cout<<"y="<= 0.999){ + std::cout<<"doing"<::Iterator j = light_sources.begin(); + for(; j != light_sources.end(); j++) + { + lightNeighbors(*j, modified_blocks); + } + + core::map::Iterator i = modified_blocks.getIterator(); + for(; i.atEnd() == false; i++) + { + i.getNode()->getValue()->updateFastFaces(); + } +} + +/* +*/ +void Map::removeNodeAndUpdate(v3s16 p) +{ + std::cout<<"Map::removeNodeAndUpdate()"< modified_blocks; + + bool node_under_sunlight = true; + + v3s16 toppos = p + v3s16(0,1,0); + + /* + If there is a node at top and it doesn't have sunlight, + there will be no sunlight going down. + */ + try{ + MapNode topnode = getNode(toppos); + + if(topnode.light < 0.999) + node_under_sunlight = false; + } + catch(InvalidPositionException &e) + { + } + + /* + Unlight neighbors (in case the node is a light source) + */ + core::list light_sources; + unLightNeighbors(p, getNode(p).light, + light_sources, modified_blocks); + + /* + Remove the node + */ + MapNode n; + n.d = MATERIAL_AIR; + setNode(p, n); + + /* + Recalculate lighting + */ + core::list::Iterator j = light_sources.begin(); + for(; j != light_sources.end(); j++) + { + lightNeighbors(*j, modified_blocks); + } + + // Add the block of the removed node to modified_blocks + v3s16 blockpos = getNodeBlockPos(p); + MapBlock * blockref = getBlockNoCreate(blockpos); + MapBlock *block = blockref; + assert(block != NULL); + modified_blocks.insert(blockpos, block); + + if(node_under_sunlight) + { + s16 ybottom = propagateSunlight(p, modified_blocks); + /*std::cout<<"Node was under sunlight. " + "Propagating sunlight"; + std::cout<<" -> ybottom="<= ybottom; y--) + { + v3s16 p2(p.X, y, p.Z); + /*std::cout<<"lighting neighbors of node (" + <::Iterator i = modified_blocks.getIterator(); + for(; i.atEnd() == false; i++) + { + i.getNode()->getValue()->updateFastFaces(); + } +} + +core::aabbox3d Map::getDisplayedBlockArea() +{ + camera_mutex.Lock(); + core::aabbox3d box_nodes(floatToInt(camera_position)); + camera_mutex.Unlock(); + + g_viewing_range_nodes_mutex.Lock(); + s16 d = g_viewing_range_nodes; + g_viewing_range_nodes_mutex.Unlock(); + box_nodes.MinEdge -= v3s16(d,d,d); + box_nodes.MaxEdge += v3s16(d,d,d); + + return core::aabbox3d( + getNodeBlockPos(box_nodes.MinEdge), + getNodeBlockPos(box_nodes.MaxEdge)); +} + +void Map::renderMap(video::IVideoDriver* driver, + video::SMaterial *materials) +{ + //std::cout<<"Rendering map..."< blocks_displayed; + core::list< MapBlock * >::Iterator bi; + +#if 0 + /* + Get all blocks + */ + core::map::Iterator si; + + si = m_sectors.getIterator(); + for(; si.atEnd() == false; si++) + { + MapSector *sector = si.getNode()->getValue(); + core::list< MapBlock * > sectorblocks = sector->getBlocks(); + core::list< MapBlock * >::Iterator i; + for(i=sectorblocks.begin(); i!=sectorblocks.end(); i++){ + blocks_displayed.push_back(*i); + } + } +#else + /* + Get visible blocks + */ + core::aabbox3d box_blocks = getDisplayedBlockArea(); + + for(s16 y=box_blocks.MaxEdge.Y; y>=box_blocks.MinEdge.Y; y--){ + for(s16 z=box_blocks.MinEdge.Z; z<=box_blocks.MaxEdge.Z; z++){ + for(s16 x=box_blocks.MinEdge.X; x<=box_blocks.MaxEdge.X; x++) + { + /* + Add block to list + */ + try{ + MapBlock * blockref = getBlockNoCreate(v3s16(x,y,z)); + blocks_displayed.push_back(blockref); + } + catch(InvalidPositionException &e) + { + } + } + } + } +#endif + + u8 material_in_use; + material_in_use = MATERIAL_AIR; + + if(blocks_displayed.getSize() == 0) + return; + + /* + Draw all displayed faces + */ + + u32 facecount = 0; + + for(bi = blocks_displayed.begin(); bi != blocks_displayed.end(); bi++){ + MapBlock *block = *bi; + + /* + Compare block position to camera position, skip + if not seen on display + */ +#if 1 + v3s16 blockpos_nodes = block->getPosRelative(); + + // Block center position + v3f blockpos( + ((float)blockpos_nodes.X + MAP_BLOCKSIZE/2) * BS, + ((float)blockpos_nodes.Y + MAP_BLOCKSIZE/2) * BS, + ((float)blockpos_nodes.Z + MAP_BLOCKSIZE/2) * BS + ); + + camera_mutex.Lock(); + // Block position relative to camera + v3f blockpos_relative = blockpos - camera_position; + + // Distance in camera direction (+=front, -=back) + f32 dforward = blockpos_relative.dotProduct(camera_direction); + + camera_mutex.Unlock(); + + // Total distance + f32 d = blockpos_relative.getLength(); + + // Maximum radius of a block + f32 block_max_radius = 0.5*1.44*1.44*MAP_BLOCKSIZE*BS; + + // If block is (nearly) touching the camera, don't + // bother checking further + if(d > block_max_radius * 1.5) + { + // Cosine of the angle between the camera direction + // and the block direction (camera_direction is an unit vector) + f32 cosangle = dforward / d; + + // Compensate for the size of the block + // (as the block has to be shown even if it's a bit off FOV) + // This is an estimate. + cosangle += block_max_radius / dforward; + + // If block is not in the field of view, skip it + if(cosangle < cos(FOV_ANGLE/2)) + continue; + } +#endif + + /* + Draw the faces of the block + */ + + block->fastfaces_mutex.Lock(); + + core::list::Iterator i = + block->fastfaces->begin(); + + for(; i != block->fastfaces->end(); i++) + { + FastFace *f = *i; + + if(f->material != material_in_use){ + driver->setMaterial(materials[f->material]); + material_in_use = f->material; + } + + u16 indices[] = {0,1,2,3}; + + driver->drawVertexPrimitiveList(f->vertices, 4, indices, 1, + video::EVT_STANDARD, scene::EPT_QUADS, video::EIT_16BIT); + + facecount++; + } + + block->fastfaces_mutex.Unlock(); + } + + static s32 oldfacecount = 0; + // Cast needed for msvc + if(abs((long)(facecount - oldfacecount)) > 333){ + std::cout<<"Rendered "< box_blocks = getDisplayedBlockArea(); + core::list< MapBlock * > blocks_changed; + core::list< MapBlock * >::Iterator bi; + + for(s16 y=box_blocks.MaxEdge.Y; y>=box_blocks.MinEdge.Y; y--){ + for(s16 z=box_blocks.MinEdge.Z; z<=box_blocks.MaxEdge.Z; z++){ + for(s16 x=box_blocks.MinEdge.X; x<=box_blocks.MaxEdge.X; x++) + { + // This intentionally creates new blocks on demand + + try + { + MapBlock * block = getBlock(v3s16(x,y,z)); + if(block->getChangedFlag()) + { + if(blocks_changed.empty() == true){ + // Print initial message when first is found + std::cout<<"Updating changed MapBlocks at "; + } + + v3s16 p = block->getPosRelative(); + std::cout<<"("<resetChangedFlag(); + } + } + catch(InvalidPositionException &e) + { + } + catch(AsyncQueuedException &e) + { + } + }}} + + // Quit if nothing has changed + if(blocks_changed.empty()){ + //status.setReady(true); + return false; + } + + std::cout< modified_blocks; + + std::cout<<"Updating lighting"<::Iterator i = modified_blocks.getIterator(); + for(; i.atEnd() == false; i++) + { + MapBlock *block = i.getNode()->getValue(); + block->updateFastFaces(); + + std::cout<<"X"; + std::cout.flush(); + + //status.setDone(status.getDone()+1); + } + + std::cout<m_heightmap->generateContinued(2.0, 0.2, corners); + //sector->getHeightmap()->generateContinued(0.0, 0.0, corners); + + MapSector *sector = new MapSector(this, p2d, gen); + + sector->setHeightmap(gen->m_heightmap); + + m_sectors.insert(p2d, sector); + + /*std::cout<<"Map::getSector(("<(0,0,0, + map->getW()*BS, map->getH()*BS, map->getD()*BS);*/ + /*m_box = core::aabbox3d(0,0,0, + map->getSizeNodes().X * BS, + map->getSizeNodes().Y * BS, + map->getSizeNodes().Z * BS);*/ + m_box = core::aabbox3d(-BS*1000000,-BS*1000000,-BS*1000000, + BS*1000000,BS*1000000,BS*1000000); +} + +MapSector * ClientMap::getSector(v2s16 p2d) +{ + // Check that it doesn't exist already + try{ + MapSector *sector = getSectorNoGenerate(p2d); + return sector; + } + catch(InvalidPositionException &e) + { + } + + /* + Create a ClientBlockGenerator to fill the MapSector's + blocks from data fetched by the client from the server. + */ + ClientBlockGenerator *gen; + gen = new ClientBlockGenerator(m_client); + + MapSector *sector = new MapSector(this, p2d, gen); + + m_sectors.insert(p2d, sector); + + return sector; +} + + diff --git a/src/map.h b/src/map.h new file mode 100644 index 0000000..b3ebc1e --- /dev/null +++ b/src/map.h @@ -0,0 +1,380 @@ +/* +(c) 2010 Perttu Ahola +*/ + +#ifndef MAP_HEADER +#define MAP_HEADER + +#include +#include +#include +#include + +#ifdef _WIN32 + #include + #define sleep_s(x) Sleep((x*1000)) +#else + #include + #define sleep_s(x) sleep(x) +#endif + +#include "common_irrlicht.h" +#include "heightmap.h" +#include "loadstatus.h" +#include "mapnode.h" +#include "mapblock.h" +#include "mapsector.h" + +/* + +TODO: Automatically unload blocks from memory and save on disk + when they are far away +*/ + +//void limitBox(core::aabbox3d & box, core::aabbox3d & limits); + +class Map; + +class MapUpdateThread : public JThread +{ + bool run; + JMutex run_mutex; + + Map *map; + +public: + + MapUpdateThread(Map *the_map) : JThread(), run(true), map(the_map) + { + run_mutex.Init(); + } + + void * Thread(); + + bool getRun() + { + run_mutex.Lock(); + bool run_cached = run; + run_mutex.Unlock(); + return run_cached; + } + void setRun(bool a_run) + { + run_mutex.Lock(); + run = a_run; + run_mutex.Unlock(); + } +}; + +class Map : public NodeContainer, public Heightmappish +{ +public: + /* + TODO: Dynamic block loading + Add a filename to the constructor, in which the map will be + automatically stored - or something. + */ + +protected: + + core::map m_sectors; + JMutex m_getsector_mutex; + JMutex m_gensector_mutex; + + v3f camera_position; + v3f camera_direction; + JMutex camera_mutex; + + MapUpdateThread updater; + + UnlimitedHeightmap m_heightmap; + + // Be sure to set this to NULL when the cached sector is deleted + MapSector *m_sector_cache; + v2s16 m_sector_cache_p; + + WrapperHeightmap m_hwrapper; + +public: + + v3s16 drawoffset; + + //LoadStatus status; + + Map(); + ~Map(); + + void updateCamera(v3f pos, v3f dir) + { + camera_mutex.Lock(); + camera_position = pos; + camera_direction = dir; + camera_mutex.Unlock(); + } + + void StartUpdater() + { + updater.Start(); + } + + void StopUpdater() + { + updater.setRun(false); + while(updater.IsRunning()) + sleep_s(1); + } + + bool UpdaterIsRunning() + { + return updater.IsRunning(); + } + + /* + Returns integer position of the node in given + floating point position. + */ + static v3s16 floatToInt(v3f p) + { + v3s16 p2( + (p.X + (p.X>0 ? BS/2 : -BS/2))/BS, + (p.Y + (p.Y>0 ? BS/2 : -BS/2))/BS, + (p.Z + (p.Z>0 ? BS/2 : -BS/2))/BS); + return p2; + } + + static v3f intToFloat(v3s16 p) + { + v3f p2( + p.X * BS, + p.Y * BS, + p.Z * BS + ); + return p2; + } + + static core::aabbox3d getNodeBox(v3s16 p) + { + return core::aabbox3d( + (float)p.X * BS - 0.5*BS, + (float)p.Y * BS - 0.5*BS, + (float)p.Z * BS - 0.5*BS, + (float)p.X * BS + 0.5*BS, + (float)p.Y * BS + 0.5*BS, + (float)p.Z * BS + 0.5*BS + ); + } + + //bool sectorExists(v2s16 p); + MapSector * getSectorNoGenerate(v2s16 p2d); + virtual MapSector * getSector(v2s16 p); + + MapBlock * getBlockNoCreate(v3s16 p); + virtual MapBlock * getBlock(v3s16 p); + + f32 getGroundHeight(v2s16 p, bool generate=false); + void setGroundHeight(v2s16 p, f32 y, bool generate=false); + + bool isValidPosition(v3s16 p) + { + v3s16 blockpos = getNodeBlockPos(p); + MapBlock * blockref = getBlockNoCreate(blockpos); + if(blockref == NULL){ + /*std::cout<<"Map::isValidPosition("<isValidPosition(relpos); + /*std::cout<<"Map::isValidPosition("<=0 ? p.X : p.X-MAP_BLOCKSIZE+1) / MAP_BLOCKSIZE, + (p.Y>=0 ? p.Y : p.Y-MAP_BLOCKSIZE+1) / MAP_BLOCKSIZE, + (p.Z>=0 ? p.Z : p.Z-MAP_BLOCKSIZE+1) / MAP_BLOCKSIZE); + } + + v2s16 getNodeSectorPos(v2s16 p) + { + return v2s16( + (p.X>=0 ? p.X : p.X-MAP_BLOCKSIZE+1) / MAP_BLOCKSIZE, + (p.Y>=0 ? p.Y : p.Y-MAP_BLOCKSIZE+1) / MAP_BLOCKSIZE); + } + + s16 getNodeBlockY(s16 y) + { + return (y>=0 ? y : y-MAP_BLOCKSIZE+1) / MAP_BLOCKSIZE; + } + + MapBlock * getNodeBlock(v3s16 p) + { + v3s16 blockpos = getNodeBlockPos(p); + return getBlock(blockpos); + } + + MapNode getNode(v3s16 p) + { + v3s16 blockpos = getNodeBlockPos(p); + MapBlock * blockref = getBlockNoCreate(blockpos); + if(blockref == NULL) + throw InvalidPositionException(); + v3s16 relpos = p - blockpos*MAP_BLOCKSIZE; + + return blockref->getNode(relpos); + } + + MapNode getNode(s16 x, s16 y, s16 z) + { + return getNode(v3s16(x,y,z)); + } + + void setNode(v3s16 p, MapNode & n) + { + v3s16 blockpos = getNodeBlockPos(p); + MapBlock * blockref = getBlockNoCreate(blockpos); + if(blockref == NULL) + throw InvalidPositionException(); + v3s16 relpos = p - blockpos*MAP_BLOCKSIZE; + blockref->setNode(relpos, n); + } + + void setNode(s16 x, s16 y, s16 z, MapNode & n) + { + setNode(v3s16(x,y,z), n); + } + + + MapNode getNode(v3f p) + { + return getNode(floatToInt(p)); + } + + void drawbox(s16 x0, s16 y0, s16 z0, s16 w, s16 h, s16 d, MapNode node) + { + x0 += drawoffset.X; + y0 += drawoffset.Y; + z0 += drawoffset.Z; + for(u16 z=0; z & light_sources, + core::map & modified_blocks); + + /*void lightNeighbors(v3s16 pos, f32 oldlight, + core::map & modified_blocks);*/ + void lightNeighbors(v3s16 pos, + core::map & modified_blocks); + + v3s16 getBrightestNeighbour(v3s16 p); + + s16 propagateSunlight(v3s16 start, + core::map & modified_blocks); + + void updateLighting(core::list< MapBlock*> & a_blocks, + core::map & modified_blocks); + + void nodeAddedUpdate(v3s16 p, f32 light); + void removeNodeAndUpdate(v3s16 p); + + core::aabbox3d getDisplayedBlockArea(); + + /*void generateBlock(MapBlock *block); + void generateMaster();*/ + + bool updateChangedVisibleArea(); + + void renderMap(video::IVideoDriver* driver, + video::SMaterial *materials); + +}; + +class MasterMap : public Map +{ +public: + MapSector * getSector(v2s16 p); +}; + +class Client; + +class ClientMap : public Map, public scene::ISceneNode +{ +public: + ClientMap( + Client *client, + video::SMaterial *materials, + scene::ISceneNode* parent, + scene::ISceneManager* mgr, + s32 id + ); + + MapSector * getSector(v2s16 p); + + /* + ISceneNode methods + */ + + virtual void OnRegisterSceneNode() + { + if (IsVisible) + SceneManager->registerNodeForRendering(this); + + ISceneNode::OnRegisterSceneNode(); + } + + virtual void render() + { + video::IVideoDriver* driver = SceneManager->getVideoDriver(); + driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); + renderMap(driver, m_materials); + } + + virtual const core::aabbox3d& getBoundingBox() const + { + return m_box; + } + + /*virtual u32 getMaterialCount() const + { + return 1; + } + + virtual video::SMaterial& getMaterial(u32 i) + { + return materials[0]; + }*/ + +private: + Client *m_client; + + video::SMaterial *m_materials; + + core::aabbox3d m_box; +}; + +#endif + diff --git a/src/mapblock.cpp b/src/mapblock.cpp new file mode 100644 index 0000000..2ff5ee2 --- /dev/null +++ b/src/mapblock.cpp @@ -0,0 +1,448 @@ +#include "mapblock.h" +#include "map.h" + +bool MapBlock::isValidPositionParent(v3s16 p) +{ + if(isValidPosition(p)) + { + //std::cout<<"[("<isValidPosition(getPosRelative() + p); + } +} + +MapNode MapBlock::getNodeParent(v3s16 p) +{ + if(isValidPosition(p) == false) + { + /*std::cout<<"MapBlock::getNodeParent((" + <getNode(getPosRelative() + p); + } + else + { + /*std::cout<<"MapBlock::getNodeParent((" + <setNode(getPosRelative() + p, n); + } + else + { + data[p.Z*MAP_BLOCKSIZE*MAP_BLOCKSIZE + p.Y*MAP_BLOCKSIZE + p.X] = n; + } +} + +FastFace * MapBlock::makeFastFace(u8 material, light_t light, v3f p, + v3f dir, v3f scale, v3f posRelative_f) +{ + FastFace *f = new FastFace; + + // Position is at the center of the cube. + v3f pos = p * BS; + posRelative_f *= BS; + + v3f vertex_pos[4]; + // If looking towards z+, this is the face that is behind + // the center point, facing towards z+. + vertex_pos[0] = v3f( BS/2,-BS/2,BS/2); + vertex_pos[1] = v3f(-BS/2,-BS/2,BS/2); + vertex_pos[2] = v3f(-BS/2, BS/2,BS/2); + vertex_pos[3] = v3f( BS/2, BS/2,BS/2); + + /* + TODO: Rotate it right + */ + core::CMatrix4 m; + m.buildRotateFromTo(v3f(0,0,1), dir); + + for(u16 i=0; i<4; i++){ + m.rotateVect(vertex_pos[i]); + vertex_pos[i].X *= scale.X; + vertex_pos[i].Y *= scale.Y; + vertex_pos[i].Z *= scale.Z; + vertex_pos[i] += pos + posRelative_f; + } + + f32 abs_scale = 1.; + if (scale.X < 0.999 || scale.X > 1.001) abs_scale = scale.X; + else if(scale.Y < 0.999 || scale.Y > 1.001) abs_scale = scale.Y; + else if(scale.Z < 0.999 || scale.Z > 1.001) abs_scale = scale.Z; + + v3f zerovector = v3f(0,0,0); + + //u8 li = (1.0+light)/2 * 255.0; //DEBUG + u8 li = light * 255.0; + //u8 li = light / 128; + video::SColor c = video::SColor(255,li,li,li); + + //video::SColor c = video::SColor(255,255,255,255); + //video::SColor c = video::SColor(255,64,64,64); + + f->vertices[0] = video::S3DVertex(vertex_pos[0], zerovector, c, + core::vector2d(0,1)); + f->vertices[1] = video::S3DVertex(vertex_pos[1], zerovector, c, + core::vector2d(abs_scale,1)); + f->vertices[2] = video::S3DVertex(vertex_pos[2], zerovector, c, + core::vector2d(abs_scale,0)); + f->vertices[3] = video::S3DVertex(vertex_pos[3], zerovector, c, + core::vector2d(0,0)); + + f->material = material; + //f->direction = direction; + + return f; +} + +/* + Parameters must consist of air and !air. + Order doesn't matter. + + If either of the nodes doesn't exist, light is 0. +*/ +light_t MapBlock::getFaceLight(v3s16 p, v3s16 face_dir) +{ + //return 1.0; + + // Make some nice difference to different sides + //float factor = ((face_dir.X != 0) ? 0.75 : 1.0); + float factor; + /*if(face_dir.X != 0) + factor = 0.70; + else if(face_dir.Z != 0) + factor = 0.90; + else + factor = 1.00;*/ + if(face_dir.X == 1 || face_dir.Z == 1) + factor = 0.70; + else if(face_dir.X == -1 || face_dir.Z == -1) + factor = 0.90; + else + factor = 1.00; + + try{ + MapNode n = getNodeParent(p); + MapNode n2 = getNodeParent(p + face_dir); + if(n.transparent()) + return n.light * factor; + else{ + return n2.light * factor; + } + } + catch(InvalidPositionException &e) + { + return 0.0; + } +} + +/* + Gets node material from any place relative to block. + Returns MATERIAL_AIR if doesn't exist. +*/ +u8 MapBlock::getNodeMaterial(v3s16 p) +{ + try{ + MapNode n = getNodeParent(p); + return n.d; + } + catch(InvalidPositionException &e) + { + return MATERIAL_IGNORE; + } +} + +/* + startpos: + translate_dir: unit vector with only one of x, y or z + face_dir: unit vector with only one of x, y or z +*/ +void MapBlock::updateFastFaceRow(v3s16 startpos, + u16 length, + v3s16 translate_dir, + v3s16 face_dir, + core::list &dest) +{ + // The maximum difference in light to tolerate in the same face + // TODO: make larger? + //light_t max_light_diff = 0.15; + light_t max_light_diff = 0.2; + + /* + Precalculate some variables + */ + v3f translate_dir_f(translate_dir.X, translate_dir.Y, + translate_dir.Z); // floating point conversion + v3f face_dir_f(face_dir.X, face_dir.Y, + face_dir.Z); // floating point conversion + v3f posRelative_f(getPosRelative().X, getPosRelative().Y, + getPosRelative().Z); // floating point conversion + + v3s16 p = startpos; + /* + The light in the air lights the surface is taken from + the node that is air. + */ + light_t light = getFaceLight(p, face_dir); + + u16 continuous_materials_count = 0; + + u8 material0 = getNodeMaterial(p); + u8 material1 = getNodeMaterial(p + face_dir); + + for(u16 j=0; j *fastfaces_new = new core::list; + + /* + These are started and stopped one node overborder to + take changes in those blocks in account. + + TODO: This could be optimized by combining it with updateLighting, as + it can record the surfaces that can be seen. + */ + + /* + Go through every y,z and get top faces in rows of x + */ + //for(s16 y=0; y *fastfaces_old = fastfaces; + + fastfaces_mutex.Lock(); + fastfaces = fastfaces_new; + fastfaces_mutex.Unlock(); + + /*std::cout<<" Number of faces: old="<getSize() + <<" new="<getSize()<::Iterator i = fastfaces_old->begin(); + for(; i != fastfaces_old->end(); i++) + { + delete *i; + } + fastfaces_old->clear(); + delete fastfaces_old; + + //std::cout<<"added "<= 0; y--){ + v3s16 pos(x, y, z); + + MapNode *n = getNodePtr(pos); + if(n == NULL) + break; + + if(n->transparent()) + { + n->light = 1.0; + } + else{ + break; + } + } + if(y == -1) + sunlight_touches_bottom = true; + } + } + + /*std::cout<<"MapBlock("< *fastfaces; + JMutex fastfaces_mutex; + + MapBlock(NodeContainer *parent, v3s16 pos) + : m_parent(parent), m_pos(pos), changed(true) + { + data = NULL; + reallocate(); + fastfaces = new core::list; + fastfaces_mutex.Init(); + } + + ~MapBlock() + { + fastfaces_mutex.Lock(); + core::list::Iterator i = fastfaces->begin(); + for(; i != fastfaces->end(); i++) + { + delete *i; + } + delete fastfaces; + fastfaces_mutex.Unlock(); + + if(data) + free(data); + } + + bool getChangedFlag() + { + return changed; + } + + void resetChangedFlag() + { + /*v3s16 p = getPosRelative(); + std::cout<<"MapBlock("< getBox() + { + return core::aabbox3d(getPosRelative(), + getPosRelative() + getSizeNodes() - v3s16(1,1,1)); + } + + void reallocate() + { + //data.clear(); + if(data != NULL) + free(data); + u32 l = MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE; + //data.reallocate(l); + data = (MapNode*)malloc(sizeof(MapNode) * l); + for(u32 i=0; i= 0 && p.X < MAP_BLOCKSIZE + && p.Y >= 0 && p.Y < MAP_BLOCKSIZE + && p.Z >= 0 && p.Z < MAP_BLOCKSIZE); + } + /* + As long as a reference of this MapBlock is kept, + the pointers returned by these can be used. + */ + MapNode * getNodePtr(s16 x, s16 y, s16 z) + { + if(x < 0 || x >= MAP_BLOCKSIZE) throw InvalidPositionException(); + if(y < 0 || y >= MAP_BLOCKSIZE) throw InvalidPositionException(); + if(z < 0 || z >= MAP_BLOCKSIZE) throw InvalidPositionException(); + return &data[z*MAP_BLOCKSIZE*MAP_BLOCKSIZE + y*MAP_BLOCKSIZE + x]; + } + MapNode * getNodePtr(v3s16 p) + { + return getNodePtr(p.X, p.Y, p.Z); + } + + /* + Regular MapNode get-setters + */ + + MapNode getNode(s16 x, s16 y, s16 z) + { + if(x < 0 || x >= MAP_BLOCKSIZE) throw InvalidPositionException(); + if(y < 0 || y >= MAP_BLOCKSIZE) throw InvalidPositionException(); + if(z < 0 || z >= MAP_BLOCKSIZE) throw InvalidPositionException(); + return data[z*MAP_BLOCKSIZE*MAP_BLOCKSIZE + y*MAP_BLOCKSIZE + x]; + } + + MapNode getNode(v3s16 p) + { + return getNode(p.X, p.Y, p.Z); + } + + void setNode(s16 x, s16 y, s16 z, MapNode & n) + { + if(x < 0 || x >= MAP_BLOCKSIZE) throw InvalidPositionException(); + if(y < 0 || y >= MAP_BLOCKSIZE) throw InvalidPositionException(); + if(z < 0 || z >= MAP_BLOCKSIZE) throw InvalidPositionException(); + data[z*MAP_BLOCKSIZE*MAP_BLOCKSIZE + y*MAP_BLOCKSIZE + x] = n; + } + + void setNode(v3s16 p, MapNode & n) + { + setNode(p.X, p.Y, p.Z, n); + } + + /* + These functions consult the parent container if the position + is not valid on this MapBlock. + */ + bool isValidPositionParent(v3s16 p); + MapNode getNodeParent(v3s16 p); + void setNodeParent(v3s16 p, MapNode & n); + + void drawbox(s16 x0, s16 y0, s16 z0, s16 w, s16 h, s16 d, MapNode node) + { + for(u16 z=0; z &dest); + + /* + Find all faces being between material and air + */ + void updateFastFaces(); + + /* + Propagates sunlight down through the block. + Returns true if some sunlight touches the bottom of the block. + */ + + bool propagateSunlight(); + + static u32 serializedLength(); + void serialize(u8 *dest); + void deSerialize(u8 *source); +}; + +#endif + diff --git a/src/mapnode.h b/src/mapnode.h new file mode 100644 index 0000000..1e74a2a --- /dev/null +++ b/src/mapnode.h @@ -0,0 +1,102 @@ +/* +(c) 2010 Perttu Ahola +*/ + +#ifndef MAPNODE_HEADER +#define MAPNODE_HEADER + +#include +#include "common_irrlicht.h" +#include "light.h" +#include "utility.h" + +// Size of node in rendering units +#define BS 10 + +#define MATERIALS_COUNT 254 + +// This is completely ignored. It doesn't create faces with anything. +#define MATERIAL_IGNORE 255 +// This is the common material through which the player can walk +// and which is transparent to light +#define MATERIAL_AIR 254 + +#define USEFUL_MATERIAL_COUNT 4 + +enum Material +{ + MATERIAL_STONE=0, + MATERIAL_GRASS, + /* + For water, the param is water pressure. 0...127. + + - Water blocks will fall down if there is empty space below. + - If there is water below, the pressure of the block below is + the pressure of the current block + 1, or higher. + - If there is any pressure in a horizontally neighboring + block, a water block will try to move away from it. + - If there is >=2 of pressure in a block below, water will + try to move upwards. + + TODO: Before implementing water, do a server-client framework. + */ + MATERIAL_WATER, + MATERIAL_TORCH, +}; + +struct MapNode +{ + //TODO: block type to differ from material (e.g. grass edges) + u8 d; // block type + light_t light; + s8 param; // Initialized to 0 + + MapNode(const MapNode & n) + { + *this = n; + } + + MapNode(u8 data=MATERIAL_AIR, light_t a_light=LIGHT_MIN, u8 a_param=0) + //MapNode(u8 data=MATERIAL_AIR, light_t a_light=LIGHT_MAX, u8 a_param=0) + { + d = data; + light = a_light; + param = a_param; + } + + /* + If true, the node allows light propagation + */ + bool transparent() + { + return (d == MATERIAL_AIR || d == MATERIAL_TORCH); + } + + light_t light_source() + { + if(d == MATERIAL_TORCH) + return 0.9; + + return 0.0; + } + + static u32 serializedLength() + { + return 2; + } + void serialize(u8 *dest) + { + dest[0] = d; + dest[1] = param; + } + void deSerialize(u8 *source) + { + d = source[0]; + param = source[1]; + } +}; + + + +#endif + diff --git a/src/mapsector.cpp b/src/mapsector.cpp new file mode 100644 index 0000000..cc7c5ec --- /dev/null +++ b/src/mapsector.cpp @@ -0,0 +1,252 @@ +#include "mapsector.h" +#include "jmutexautolock.h" +#include "client.h" +#include "exceptions.h" + +HeightmapBlockGenerator::HeightmapBlockGenerator + (v2s16 p2d, Heightmap * masterheightmap): + m_heightmap(NULL) +{ + m_heightmap = new FixedHeightmap(masterheightmap, + p2d, MAP_BLOCKSIZE); +} + +/*HeightmapBlockGenerator::HeightmapBlockGenerator + (MapSector *sector, Heightmap * masterheightmap): + m_heightmap(NULL) +{ + m_heightmap = new FixedHeightmap(masterheightmap, + sector->getPos(), MAP_BLOCKSIZE); + sector->setHeightmap(m_heightmap); +}*/ + +MapBlock * HeightmapBlockGenerator::makeBlock(MapSector *sector, s16 block_y) +{ + MapBlock *block = sector->createBlankBlockNoInsert(block_y); + + // Randomize a bit. This makes dungeons. + bool low_block_is_empty = false; + if(rand() % 4 == 0) + low_block_is_empty = true; + + u8 material = MATERIAL_GRASS; + s32 avg_ground_y = 0.0; + + for(s16 z0=0; z0getGroundHeight(v2s16(x0,z0)); + avg_ground_y += target_y; + + if(m_heightmap->getSlope(v2s16(x0,z0)).getLength() > 1.05) + material = MATERIAL_STONE; + else + material = MATERIAL_GRASS; + + for(s16 y0=0; y0setNode(v3s16(x0,y0,z0), n); + } + } + } + + avg_ground_y /= MAP_BLOCKSIZE * MAP_BLOCKSIZE; + bool probably_dark = (block_y+1) * MAP_BLOCKSIZE < avg_ground_y; + block->setProbablyDark(probably_dark); + + return block; +} + +MapBlock * ClientBlockGenerator::makeBlock(MapSector *sector, s16 block_y) +{ + //std::cout<<"ClientBlockGenerator::makeBlock()"<getPos().X, block_y, sector->getPos().Y); + + //m_client->addToBlockFetchQueue(blockpos_map); + m_client->fetchBlock(blockpos_map); + + throw AsyncQueuedException("Client will fetch later"); +} + +MapBlock * MapSector::getBlockBuffered(s16 y) +{ + MapBlock *block; + + if(m_block_cache != NULL && y == m_block_cache_y){ + return m_block_cache; + } + + // If block doesn't exist, return NULL + core::map::Node *n = m_blocks.find(y); + if(n == NULL) + { + /* + TODO: Check if block is stored on disk + and load dynamically + */ + block = NULL; + } + // If block exists, return it + else{ + block = n->getValue(); + } + + // Cache the last result + m_block_cache_y = y; + m_block_cache = block; + + return block; +} + +MapBlock * MapSector::getBlockNoCreate(s16 y) +{ + /*std::cout<<"MapSector("<makeBlock(this, block_y); + + //block->propagateSunlight(); + + // Insert to container + { + JMutexAutoLock lock(m_mutex); + m_blocks.insert(block_y, block); + } + + return block; +} + +void MapSector::insertBlock(MapBlock *block) +{ + s16 block_y = block->getPos().Y; + + { + JMutexAutoLock lock(m_mutex); + MapBlock *block = getBlockBuffered(block_y); + if(block != NULL){ + throw AlreadyExistsException("Block already exists"); + } + } + + v2s16 p2d(block->getPos().X, block->getPos().Z); + assert(p2d == m_pos); + + block->propagateSunlight(); + + // Insert to container + { + JMutexAutoLock lock(m_mutex); + m_blocks.insert(block_y, block); + } +} + +core::list MapSector::getBlocks() +{ + JMutexAutoLock lock(m_mutex); + + core::list ref_list; + + core::map::Iterator bi; + + bi = m_blocks.getIterator(); + for(; bi.atEnd() == false; bi++) + { + MapBlock *b = bi.getNode()->getValue(); + ref_list.push_back(b); + } + + return ref_list; +} + +f32 MapSector::getGroundHeight(v2s16 p, bool generate) +{ + if(m_heightmap == NULL) + return GROUNDHEIGHT_NOTFOUND_SETVALUE; + return m_heightmap->getGroundHeight(p); +} + +void MapSector::setGroundHeight(v2s16 p, f32 y, bool generate) +{ + if(m_heightmap == NULL) + return; + m_heightmap->setGroundHeight(p, y); +} + +//END diff --git a/src/mapsector.h b/src/mapsector.h new file mode 100644 index 0000000..9618cca --- /dev/null +++ b/src/mapsector.h @@ -0,0 +1,156 @@ +/* +(c) 2010 Perttu Ahola +*/ + +#ifndef MAPSECTOR_HEADER +#define MAPSECTOR_HEADER + +#include +#include "common_irrlicht.h" +#include "mapblock.h" +#include "heightmap.h" +#include "exceptions.h" + +/* + This is an Y-wise stack of MapBlocks. + + TODO: + - Add heightmap functionality + - Implement node container functionality and modify Map to use it +*/ + +class MapSector; + +class BlockGenerator +{ +public: + virtual MapBlock * makeBlock(MapSector *sector, s16 block_y) + { + assert(0); + return NULL; + } +}; + +class HeightmapBlockGenerator : public BlockGenerator +{ +public: + HeightmapBlockGenerator(v2s16 p2d, Heightmap * masterheightmap); + //HeightmapBlockGenerator(MapSector *sector, Heightmap * masterheightmap); + + ~HeightmapBlockGenerator() + { + delete m_heightmap; + } + + MapBlock * makeBlock(MapSector *sector, s16 block_y); + + // This should be generated before usage of the class + FixedHeightmap *m_heightmap; +}; + +class Client; + +/* + A block "generator" class that fetches blocks + using the client from the server +*/ +class ClientBlockGenerator : public BlockGenerator +{ +public: + ClientBlockGenerator(Client * client) + { + m_client = client; + } + ~ClientBlockGenerator() + { + } + MapBlock * makeBlock(MapSector *sector, s16 block_y); +private: + Client *m_client; +}; + +class MapSector : + public NodeContainer, + public Heightmappish +{ +private: + + // The pile of MapBlocks + core::map m_blocks; + //JMutex m_blocks_mutex; // For public access functions + + NodeContainer *m_parent; + // Position on parent (in MapBlock widths) + v2s16 m_pos; + + MapBlock *getBlockBuffered(s16 y); + + // Be sure to set this to NULL when the cached block is deleted + MapBlock *m_block_cache; + s16 m_block_cache_y; + + JMutex m_mutex; + + // This is a pointer to the generator's heightmap if applicable + FixedHeightmap *m_heightmap; + + BlockGenerator *m_block_gen; + +public: + + MapSector(NodeContainer *parent, v2s16 pos, BlockGenerator *gen): + m_parent(parent), + m_pos(pos), + m_block_cache(NULL), + m_heightmap(NULL), + m_block_gen(gen) + { + m_mutex.Init(); + assert(m_mutex.IsInitialized()); + } + + ~MapSector() + { + //TODO: clear m_blocks + core::map::Iterator i = m_blocks.getIterator(); + for(; i.atEnd() == false; i++) + { + delete i.getNode()->getValue(); + } + + delete m_block_gen; + } + + /*FixedHeightmap * getHeightmap() + { + if(m_heightmap == NULL) + throw TargetInexistentException(); + return m_heightmap; + }*/ + + void setHeightmap(FixedHeightmap *fh) + { + m_heightmap = fh; + } + + v2s16 getPos() + { + return m_pos; + } + + MapBlock * getBlockNoCreate(s16 y); + MapBlock * createBlankBlockNoInsert(s16 y); + MapBlock * createBlankBlock(s16 y); + MapBlock * getBlock(s16 y); + + void insertBlock(MapBlock *block); + + core::list getBlocks(); + + // These have to exist + f32 getGroundHeight(v2s16 p, bool generate=false); + void setGroundHeight(v2s16 p, f32 y, bool generate=false); +}; + +#endif + diff --git a/src/player.cpp b/src/player.cpp new file mode 100644 index 0000000..ca5db0a --- /dev/null +++ b/src/player.cpp @@ -0,0 +1,196 @@ +/* +(c) 2010 Perttu Ahola +*/ + +#include "player.h" +#include "map.h" +#include "connection.h" + +Player::Player(bool is_local): + scene::ISceneNode(NULL, NULL, 0), + speed(0,0,0), + touching_ground(false), + peer_id(PEER_ID_NEW), + timeout_counter(0.0), + m_is_local(is_local), + m_position(0,0,0) +{ +} + +Player::Player( + bool is_local, + scene::ISceneNode* parent, + scene::ISceneManager* mgr, + s32 id): + scene::ISceneNode(parent, mgr, id), + speed(0,0,0), + touching_ground(false), + peer_id(PEER_ID_NEW), + timeout_counter(0.0), + m_is_local(is_local), + m_position(0,0,0) +{ + m_box = core::aabbox3d(-BS,-BS,-BS,BS,BS,BS); + + m_bill = NULL; + + if(is_local == false) + { + // attach billboard to the light + m_bill = mgr->addBillboardSceneNode(this, core::dimension2d(60, 60)); + + // ISceneNode stores a member called SceneManager + video::IVideoDriver* driver = SceneManager->getVideoDriver(); + + m_bill->setMaterialFlag(video::EMF_LIGHTING, false); + //m_bill->setMaterialFlag(video::EMF_ZWRITE_ENABLE, false); + //m_bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR); + m_bill->setMaterialTexture(0, driver->getTexture("../data/tf.jpg")); + m_bill->setSize(core::dimension2d(BS,BS)); + + m_bill->setPosition(v3f(0, BS+BS/3, 0)); + + updateSceneNodePosition(); + } +} + +Player::~Player() +{ + if(SceneManager != NULL) + ISceneNode::remove(); +} + +void Player::move(f32 dtime, Map &map) +{ + v3f position = getPosition(); + v3f oldpos = position; + v3s16 oldpos_i = Map::floatToInt(oldpos); + + /*std::cout<<"oldpos_i=("< playerbox( + position.X - PLAYER_RADIUS, + position.Y - 0.0, + position.Z - PLAYER_RADIUS, + position.X + PLAYER_RADIUS, + position.Y + PLAYER_HEIGHT, + position.Z + PLAYER_RADIUS + ); + core::aabbox3d playerbox_old( + oldpos.X - PLAYER_RADIUS, + oldpos.Y - 0.0, + oldpos.Z - PLAYER_RADIUS, + oldpos.X + PLAYER_RADIUS, + oldpos.Y + PLAYER_HEIGHT, + oldpos.Z + PLAYER_RADIUS + ); + + //hilightboxes.push_back(playerbox); + + touching_ground = false; + + /*std::cout<<"Checking collisions for (" + < (" + < nodebox = Map::getNodeBox( + v3s16(x,y,z)); + + // See if the player is touching ground + if( + /*(nodebox.MaxEdge.Y+d > playerbox.MinEdge.Y && + nodebox.MaxEdge.Y-d < playerbox.MinEdge.Y)*/ + fabs(nodebox.MaxEdge.Y-playerbox.MinEdge.Y) < d + && nodebox.MaxEdge.X-d > playerbox.MinEdge.X + && nodebox.MinEdge.X+d < playerbox.MaxEdge.X + && nodebox.MaxEdge.Z-d > playerbox.MinEdge.Z + && nodebox.MinEdge.Z+d < playerbox.MaxEdge.Z + ){ + touching_ground = true; + } + + if(playerbox.intersectsWithBox(nodebox)) + { + + v3f dirs[3] = { + v3f(0,0,1), // back + v3f(0,1,0), // top + v3f(1,0,0), // right + }; + for(u16 i=0; i<3; i++) + { + f32 nodemax = nodebox.MaxEdge.dotProduct(dirs[i]); + f32 nodemin = nodebox.MinEdge.dotProduct(dirs[i]); + f32 playermax = playerbox.MaxEdge.dotProduct(dirs[i]); + f32 playermin = playerbox.MinEdge.dotProduct(dirs[i]); + f32 playermax_old = playerbox_old.MaxEdge.dotProduct(dirs[i]); + f32 playermin_old = playerbox_old.MinEdge.dotProduct(dirs[i]); + + bool main_edge_collides = + ((nodemax > playermin && nodemax <= playermin_old + d + && speed.dotProduct(dirs[i]) < 0) + || + (nodemin < playermax && nodemin >= playermax_old - d + && speed.dotProduct(dirs[i]) > 0)); + + bool other_edges_collide = true; + for(u16 j=0; j<3; j++) + { + if(j == i) + continue; + f32 nodemax = nodebox.MaxEdge.dotProduct(dirs[j]); + f32 nodemin = nodebox.MinEdge.dotProduct(dirs[j]); + f32 playermax = playerbox.MaxEdge.dotProduct(dirs[j]); + f32 playermin = playerbox.MinEdge.dotProduct(dirs[j]); + if(!(nodemax - d > playermin && nodemin + d < playermax)) + { + other_edges_collide = false; + break; + } + } + + if(main_edge_collides && other_edges_collide) + { + speed -= speed.dotProduct(dirs[i]) * dirs[i]; + position -= position.dotProduct(dirs[i]) * dirs[i]; + position += oldpos.dotProduct(dirs[i]) * dirs[i]; + } + + } + } // if(playerbox.intersectsWithBox(nodebox)) + } // for x + } // for z + } // for y + + setPosition(position); +} + diff --git a/src/player.h b/src/player.h new file mode 100644 index 0000000..6b5802b --- /dev/null +++ b/src/player.h @@ -0,0 +1,93 @@ +/* +(c) 2010 Perttu Ahola +*/ + +#ifndef PLAYER_HEADER +#define PLAYER_HEADER + +#include + +using namespace irr; +typedef core::vector3df v3f; +typedef core::vector3d v3s16; + +class Map; + +/* + TODO: Make this a scene::ISceneNode +*/ + +class Player : public scene::ISceneNode +{ +public: + Player(bool is_local); + + Player( + bool is_local, + scene::ISceneNode* parent, + scene::ISceneManager* mgr, + s32 id); + + ~Player(); + + void move(f32 dtime, Map &map); + + /* + ISceneNode methods + */ + + virtual void OnRegisterSceneNode() + { + if (IsVisible) + SceneManager->registerNodeForRendering(this); + + ISceneNode::OnRegisterSceneNode(); + } + + virtual void render() + { + // Do nothing + } + + virtual const core::aabbox3d& getBoundingBox() const + { + return m_box; + } + + v3f getPosition() + { + return m_position; + } + + void setPosition(v3f position) + { + m_position = position; + updateSceneNodePosition(); + } + + void updateSceneNodePosition() + { + ISceneNode::setPosition(m_position); + } + + bool isLocal() + { + return m_is_local; + } + + v3f speed; + bool touching_ground; + u16 peer_id; + float timeout_counter; + +private: + bool m_is_local; + v3f m_position; + //scene::ISceneNode* m_bill; + scene::IBillboardSceneNode* m_bill; + + core::aabbox3d m_box; +}; + +#endif + diff --git a/src/server.cpp b/src/server.cpp new file mode 100644 index 0000000..4316ddc --- /dev/null +++ b/src/server.cpp @@ -0,0 +1,313 @@ +#include "server.h" +#include "utility.h" +#include +#include "clientserver.h" +#include "map.h" +#include "jmutexautolock.h" +#include "main.h" + +#ifdef _WIN32 + #include + #define sleep_ms(x) Sleep(x) +#else + #include + #define sleep_ms(x) usleep(x*1000) +#endif + +void * ServerNetworkThread::Thread() +{ + ThreadStarted(); + + while(getRun()) + { + m_server->AsyncRunStep(); + + try{ + //dout_server<<"Running m_server->Receive()"<Receive(); + } + catch(con::NoIncomingDataException &e) + { + } + catch(std::exception &e) + { + dout_server<<"ServerNetworkThread: Some exception: " + < data(data_maxsize); + u16 peer_id; + u32 datasize; + try{ + { + JMutexAutoLock lock(m_con_mutex); + datasize = m_con.Receive(peer_id, *data, data_maxsize); + } + ProcessData(*data, datasize, peer_id); + } + catch(con::InvalidIncomingDataException &e) + { + dout_server<<"Server::Receive(): " + "InvalidIncomingDataException: what()=" + < reply(replysize); + writeU16(&reply[0], TOCLIENT_BLOCKDATA); + writeS16(&reply[2], p.X); + writeS16(&reply[4], p.Y); + writeS16(&reply[6], p.Z); + block->serialize(&reply[8]); + // Send as unreliable + //m_con.Send(peer_id, 1, reply, false); + m_con.Send(peer_id, 1, reply, true); + } + else if(command == TOSERVER_REMOVENODE) + { + if(datasize < 8) + return; + + v3s16 p; + p.X = readS16(&data[2]); + p.Y = readS16(&data[4]); + p.Z = readS16(&data[6]); + + MapNode n; + n.d = MATERIAL_AIR; + m_env.getMap().setNode(p, n); + + u32 replysize = 8; + //u8 reply[replysize]; + SharedBuffer reply(replysize); + writeU16(&reply[0], TOCLIENT_REMOVENODE); + writeS16(&reply[2], p.X); + writeS16(&reply[4], p.Y); + writeS16(&reply[6], p.Z); + // Send as reliable + m_con.SendToAll(0, reply, true); + } + else if(command == TOSERVER_ADDNODE) + { + if(datasize < 8 + MapNode::serializedLength()) + return; + + v3s16 p; + p.X = readS16(&data[2]); + p.Y = readS16(&data[4]); + p.Z = readS16(&data[6]); + + MapNode n; + n.deSerialize(&data[8]); + m_env.getMap().setNode(p, n); + + u32 replysize = 8 + MapNode::serializedLength(); + //u8 reply[replysize]; + SharedBuffer reply(replysize); + writeU16(&reply[0], TOCLIENT_ADDNODE); + writeS16(&reply[2], p.X); + writeS16(&reply[4], p.Y); + writeS16(&reply[6], p.Z); + n.serialize(&reply[8]); + // Send as reliable + m_con.SendToAll(0, reply, true); + } + else if(command == TOSERVER_PLAYERPOS) + { + if(datasize < 2+12+12) + return; + + Player *player = m_env.getPlayer(peer_id); + + // Create a player if it doesn't exist + if(player == NULL) + { + dout_server<<"Server::ProcessData(): Adding player " + <peer_id = peer_id; + m_env.addPlayer(player); + } + + player->timeout_counter = 0.0; + + v3s32 ps = readV3S32(&data[2]); + v3s32 ss = readV3S32(&data[2+12]); + v3f position((f32)ps.X/100., (f32)ps.Y/100., (f32)ps.Z/100.); + v3f speed((f32)ss.X/100., (f32)ss.Y/100., (f32)ss.Z/100.); + player->setPosition(position); + player->speed = speed; + + /*dout_server<<"Server::ProcessData(): Moved player "< +#include +#include + +// Debug print options +#define DP 0 +//#define DPS " " +#define DPS "" + +bool g_sockets_initialized = false; + +void sockets_init() +{ +#ifdef _WIN32 + WSADATA WsaData; + if(WSAStartup( MAKEWORD(2,2), &WsaData ) != NO_ERROR) + throw SocketException("WSAStartup failed"); +#else +#endif + g_sockets_initialized = true; +} + +void sockets_cleanup() +{ +#ifdef _WIN32 + WSACleanup(); +#endif +} + +Address::Address() +{ +} + +Address::Address(unsigned int address, unsigned short port) +{ + m_address = address; + m_port = port; +} + +Address::Address(unsigned int a, unsigned int b, + unsigned int c, unsigned int d, + unsigned short port) +{ + m_address = (a<<24) | (b<<16) | ( c<<8) | d; + m_port = port; +} + +bool Address::operator==(Address &address) +{ + return (m_address == address.m_address + && m_port == address.m_port); +} + +void Address::Resolve(const char *name) +{ + struct addrinfo *resolved; + int e = getaddrinfo(name, NULL, NULL, &resolved); + if(e != 0) + throw ResolveError(""); + /* + FIXME: This is an ugly hack; change the whole class + to store the address as sockaddr + */ + struct sockaddr_in *t = (struct sockaddr_in*)resolved->ai_addr; + m_address = ntohl(t->sin_addr.s_addr); + freeaddrinfo(resolved); +} + +unsigned int Address::getAddress() const +{ + return m_address; +} + +unsigned short Address::getPort() const +{ + return m_port; +} + +void Address::setAddress(unsigned int address) +{ + m_address = address; +} + +void Address::setPort(unsigned short port) +{ + m_port = port; +} + +void Address::print() const +{ + std::cout<<((m_address>>24)&0xff)<<"." + <<((m_address>>16)&0xff)<<"." + <<((m_address>>8)&0xff)<<"." + <<((m_address>>0)&0xff)<<":" + < "; + destination.print(); + std::cout<<", size="<20) + std::cout<<"..."; + if(dumping_packet) + std::cout<<" (DUMPED BY INTERNET_SIMULATOR)"; + std::cout<20) + std::cout<<"..."; + std::cout< + #include + #include + #pragma comment(lib, "wsock32.lib") +typedef SOCKET socket_t; +typedef int socklen_t; +#else + #include + #include + #include + #include + #include +typedef int socket_t; +#endif + +#include "exceptions.h" + +// Define for simulating the quirks of sending through internet +// WARNING: This disables unit testing of socket and connection +#define INTERNET_SIMULATOR 0 + +class SocketException : public BaseException +{ +public: + SocketException(const char *s): + BaseException(s) + { + } +}; + +class ResolveError : public BaseException +{ +public: + ResolveError(const char *s): + BaseException(s) + { + } +}; + +class SendFailedException : public BaseException +{ +public: + SendFailedException(const char *s): + BaseException(s) + { + } +}; + +void sockets_init(); +void sockets_cleanup(); + +class Address +{ +public: + Address(); + Address(unsigned int address, unsigned short port); + Address(unsigned int a, unsigned int b, + unsigned int c, unsigned int d, + unsigned short port); + bool operator==(Address &address); + void Resolve(const char *name); + unsigned int getAddress() const; + unsigned short getPort() const; + void setAddress(unsigned int address); + void setPort(unsigned short port); + void print() const; +private: + unsigned int m_address; + unsigned short m_port; +}; + +class UDPSocket +{ +public: + UDPSocket(); + ~UDPSocket(); + void Bind(unsigned short port); + //void Close(); + //bool IsOpen(); + void Send(const Address & destination, const void * data, int size); + // Returns -1 if there is no data + int Receive(Address & sender, void * data, int size); + int GetHandle(); // For debugging purposes only + void setTimeoutMs(int timeout_ms); + // Returns true if there is data, false if timeout occurred + bool WaitData(int timeout_ms); +private: + int m_handle; + int m_timeout_ms; +}; + +#endif + diff --git a/src/test.cpp b/src/test.cpp new file mode 100644 index 0000000..939acc7 --- /dev/null +++ b/src/test.cpp @@ -0,0 +1,744 @@ +#include "test.h" +#include "common_irrlicht.h" + +#include "map.h" +#include "player.h" +#include "main.h" +//#include "referencecounted.h" +#include "heightmap.h" +#include "socket.h" +#include "connection.h" +#include "utility.h" + +#ifdef _WIN32 + #include + #define sleep_ms(x) Sleep(x) +#else + #include + #define sleep_ms(x) usleep(x*1000) +#endif + +/* + Asserts that the exception occurs +*/ +#define EXCEPTION_CHECK(EType, code)\ +{\ + bool exception_thrown = false;\ + try{ code; }\ + catch(EType &e) { exception_thrown = true; }\ + assert(exception_thrown);\ +} + + +/*struct TestRefcount +{ + class TC : public ReferenceCounted + { + public: + s16 value; + TC() : ReferenceCounted("TC") + { + value = 0; + } + }; + + Ref TestReturn() + { + static TC tc; + tc.value = 5; + return &tc; + } + + void Run() + { + TC tc; + + tc.grab(); + assert(tc.getRefcount() == 1); + + tc.drop(); + assert(tc.getRefcount() == 0); + + bool exception = false; + try + { + tc.drop(); + } + catch(ReferenceCounted::Exception & e) + { + exception = true; + } + assert(exception == true); + + TC tc2; + { + Ref ref(&tc2); + assert(ref.get() == &tc2); + assert(tc2.getRefcount() == 1); + } + assert(tc2.getRefcount() == 0); + + Ref ref2 = TestReturn(); + assert(ref2.get()->value == 5); + assert(ref2.get()->getRefcount() == 1); + } +};*/ + +struct TestMapNode +{ + void Run() + { + MapNode n; + + // Default values + assert(n.d == MATERIAL_AIR); + assert(n.light == 0.0); + + // Transparency + n.d = MATERIAL_AIR; + assert(n.transparent() == true); + n.d = 0; + assert(n.transparent() == false); + } +}; + +struct TestMapBlock +{ + class TC : public NodeContainer + { + public: + + MapNode node; + bool position_valid; + + TC() + { + position_valid = true; + } + + virtual bool isValidPosition(v3s16 p) + { + return position_valid; + } + + virtual MapNode getNode(v3s16 p) + { + if(position_valid == false) + throw InvalidPositionException(); + return node; + } + + virtual void setNode(v3s16 p, MapNode & n) + { + if(position_valid == false) + throw InvalidPositionException(); + }; + }; + + void Run() + { + TC parent; + + MapBlock b(&parent, v3s16(1,1,1)); + v3s16 relpos(MAP_BLOCKSIZE, MAP_BLOCKSIZE, MAP_BLOCKSIZE); + + assert(b.getPosRelative() == relpos); + + assert(b.getBox().MinEdge.X == MAP_BLOCKSIZE); + assert(b.getBox().MaxEdge.X == MAP_BLOCKSIZE*2-1); + assert(b.getBox().MinEdge.Y == MAP_BLOCKSIZE); + assert(b.getBox().MaxEdge.Y == MAP_BLOCKSIZE*2-1); + assert(b.getBox().MinEdge.Z == MAP_BLOCKSIZE); + assert(b.getBox().MaxEdge.Z == MAP_BLOCKSIZE*2-1); + + assert(b.isValidPosition(v3s16(0,0,0)) == true); + assert(b.isValidPosition(v3s16(-1,0,0)) == false); + assert(b.isValidPosition(v3s16(-1,-142,-2341)) == false); + assert(b.isValidPosition(v3s16(-124,142,2341)) == false); + assert(b.isValidPosition(v3s16(MAP_BLOCKSIZE-1,MAP_BLOCKSIZE-1,MAP_BLOCKSIZE-1)) == true); + assert(b.isValidPosition(v3s16(MAP_BLOCKSIZE-1,MAP_BLOCKSIZE,MAP_BLOCKSIZE-1)) == false); + + /* + TODO: this method should probably be removed + if the block size isn't going to be set variable + */ + assert(b.getSizeNodes() == v3s16(MAP_BLOCKSIZE, + MAP_BLOCKSIZE, MAP_BLOCKSIZE)); + + // Changed flag should be initially set + assert(b.getChangedFlag() == true); + b.resetChangedFlag(); + assert(b.getChangedFlag() == false); + + // All nodes should have been set to + // .d=MATERIAL_AIR and .light < 0.001 + for(u16 z=0; z 0.999); + assert(b.getNode(v3s16(1,3,0)).light > 0.999); + assert(b.getNode(v3s16(1,2,0)).light < 0.001); + assert(b.getNode(v3s16(1,1,0)).light < 0.001); + assert(b.getNode(v3s16(1,0,0)).light < 0.001); + assert(b.getNode(v3s16(1,2,3)).light > 0.999); + assert(b.getFaceLight(p, v3s16(0,1,0)) > 0.999); + assert(b.getFaceLight(p, v3s16(0,-1,0)) < 0.001); + assert(fabs(b.getFaceLight(p, v3s16(0,0,1))-0.7) < 0.001); + parent.position_valid = true; + parent.node.light = 0.500; + assert(b.propagateSunlight() == false); + // Should not touch blocks that are not affected (that is, all of them) + assert(b.getNode(v3s16(1,2,3)).light > 0.999); + } +}; + +struct TestMapSector +{ + class TC : public NodeContainer + { + public: + + MapNode node; + bool position_valid; + + TC() + { + position_valid = true; + } + + virtual bool isValidPosition(v3s16 p) + { + return position_valid; + } + + virtual MapNode getNode(v3s16 p) + { + if(position_valid == false) + throw InvalidPositionException(); + return node; + } + + virtual void setNode(v3s16 p, MapNode & n) + { + if(position_valid == false) + throw InvalidPositionException(); + }; + }; + + void Run() + { + TC parent; + parent.position_valid = false; + + DummyHeightmap dummyheightmap; + + HeightmapBlockGenerator *gen = + new HeightmapBlockGenerator(v2s16(1,1), &dummyheightmap); + MapSector sector(&parent, v2s16(1,1), gen); + + EXCEPTION_CHECK(InvalidPositionException, sector.getBlockNoCreate(0)); + EXCEPTION_CHECK(InvalidPositionException, sector.getBlockNoCreate(1)); + + MapBlock * bref = sector.createBlankBlock(-2); + + EXCEPTION_CHECK(InvalidPositionException, sector.getBlockNoCreate(0)); + assert(sector.getBlockNoCreate(-2) == bref); + + //TODO: Check for AlreadyExistsException + + /*bool exception_thrown = false; + try{ + sector.getBlock(0); + } + catch(InvalidPositionException &e){ + exception_thrown = true; + } + assert(exception_thrown);*/ + + } +}; + +struct TestHeightmap +{ + void TestSingleFixed() + { + const s16 BS1 = 4; + OneChildHeightmap hm1(BS1); + + // Test that it is filled with < GROUNDHEIGHT_VALID_MINVALUE + for(s16 y=0; y<=BS1; y++){ + for(s16 x=0; x<=BS1; x++){ + v2s16 p(x,y); + assert(hm1.m_child.getGroundHeight(p) + < GROUNDHEIGHT_VALID_MINVALUE); + } + } + + hm1.m_child.setGroundHeight(v2s16(1,0), 2.0); + //hm1.m_child.print(); + assert(fabs(hm1.getGroundHeight(v2s16(1,0))-2.0)<0.001); + hm1.setGroundHeight(v2s16(0,1), 3.0); + assert(fabs(hm1.m_child.getGroundHeight(v2s16(0,1))-3.0)<0.001); + + // Fill with -1.0 + for(s16 y=0; y<=BS1; y++){ + for(s16 x=0; x<=BS1; x++){ + v2s16 p(x,y); + hm1.m_child.setGroundHeight(p, -1.0); + } + } + + f32 corners[] = {0.0, 0.0, 1.0, 1.0}; + hm1.m_child.generateContinued(0.0, 0.0, corners); + + hm1.m_child.print(); + assert(fabs(hm1.m_child.getGroundHeight(v2s16(1,0))-0.2)<0.05); + assert(fabs(hm1.m_child.getGroundHeight(v2s16(4,3))-0.7)<0.05); + assert(fabs(hm1.m_child.getGroundHeight(v2s16(4,4))-1.0)<0.05); + } + + void TestUnlimited() + { + //g_heightmap_debugprint = true; + const s16 BS1 = 4; + UnlimitedHeightmap hm1(BS1, 0.0, 0.0, 5.0); + // Go through it so it generates itself + for(s16 y=0; y<=BS1; y++){ + for(s16 x=0; x<=BS1; x++){ + v2s16 p(x,y); + hm1.getGroundHeight(p); + } + } + // Print it + std::cout<<"UnlimitedHeightmap hm1:"<setGroundHeight(p1, v1); + // Read from UnlimitedHeightmap + assert(fabs(hm1.getGroundHeight(p1)-v1)<0.001); + } + + void Random() + { + std::cout<<"Running random code"<generateContinued(0.0, 0.0, corners); + hm1.print();*/ + } + + void Run() + { + //srand(7); // Get constant random + srand(time(0)); // Get better random + + TestSingleFixed(); + TestUnlimited(); + Random(); + + g_heightmap_debugprint = false; + //g_heightmap_debugprint = true; + } +}; + +struct TestSocket +{ + void Run() + { + const int port = 30003; + UDPSocket socket; + socket.Bind(port); + + const char sendbuffer[] = "hello world!"; + socket.Send(Address(127,0,0,1,port), sendbuffer, sizeof(sendbuffer)); + + sleep_ms(50); + + char rcvbuffer[256]; + memset(rcvbuffer, 0, sizeof(rcvbuffer)); + Address sender; + for(;;) + { + int bytes_read = socket.Receive(sender, rcvbuffer, sizeof(rcvbuffer)); + if(bytes_read < 0) + break; + } + assert(strncmp(sendbuffer, rcvbuffer, sizeof(rcvbuffer))==0); + assert(sender.getAddress() == Address(127,0,0,1, 0).getAddress()); + } +}; + +struct TestConnection +{ + void TestHelpers() + { + /* + Test helper functions + */ + + // Some constants for testing + u32 proto_id = 0x12345678; + u16 peer_id = 123; + u8 channel = 2; + SharedBuffer data1(1); + data1[0] = 100; + Address a(127,0,0,1, 10); + u16 seqnum = 34352; + + con::BufferedPacket p1 = con::makePacket(a, data1, + proto_id, peer_id, channel); + /* + We should now have a packet with this data: + Header: + [0] u32 protocol_id + [4] u16 sender_peer_id + [6] u8 channel + Data: + [7] u8 data1[0] + */ + assert(readU32(&p1.data[0]) == proto_id); + assert(readU16(&p1.data[4]) == peer_id); + assert(readU8(&p1.data[6]) == channel); + assert(readU8(&p1.data[7]) == data1[0]); + + SharedBuffer p2 = con::makeReliablePacket(data1, seqnum); + assert(readU8(&p2[0]) == TYPE_RELIABLE); + assert(readU16(&p2[1]) == seqnum); + assert(readU8(&p2[3]) == data1[0]); + } + void Run() + { + TestHelpers(); + + /* + Test some real connections + */ + u32 proto_id = 0xad26846a; + + std::cout<<"** Creating server Connection"< data = SharedBufferFromString("Hello World!"); + + std::cout<<"** running client.Send()"< data1 = SharedBufferFromString("hello1"); + SharedBuffer data2 = SharedBufferFromString("Hello2"); + + Address client_address = + server.GetPeer(peer_id_client)->address; + + std::cout<<"*** Sending packets in wrong order (2,1,2)" + <channels[chn]; + u16 sn = ch->next_outgoing_seqnum; + ch->next_outgoing_seqnum = sn+1; + server.Send(peer_id_client, chn, data2, true); + ch->next_outgoing_seqnum = sn; + server.Send(peer_id_client, chn, data1, true); + ch->next_outgoing_seqnum = sn+1; + server.Send(peer_id_client, chn, data2, true); + + sleep_ms(50); + + std::cout<<"*** Receiving the packets"< data1(1100); + for(u16 i=0; i<1100; i++){ + data1[i] = i/4; + } + + std::cout<<"Sending data (size="<<1100<<"):"; + for(int i=0; i<1100 && i<20; i++){ + if(i%2==0) printf(" "); + printf("%.2X", ((int)((const char*)*data1)[i])&0xff); + } + if(1100>20) + std::cout<<"..."; + std::cout<20) + std::cout<<"..."; + std::cout<>24)&0xff); + data[1] = ((i>>16)&0xff); + data[2] = ((i>> 8)&0xff); + data[3] = ((i>> 0)&0xff); +} + +void writeU16(u8 *data, u16 i) +{ + data[0] = ((i>> 8)&0xff); + data[1] = ((i>> 0)&0xff); +} + +void writeU8(u8 *data, u8 i) +{ + data[0] = ((i>> 0)&0xff); +} + +u32 readU32(u8 *data) +{ + return (data[0]<<24) | (data[1]<<16) | (data[2]<<8) | (data[3]<<0); +} + +u16 readU16(u8 *data) +{ + return (data[0]<<8) | (data[1]<<0); +} + +u8 readU8(u8 *data) +{ + return (data[0]<<0); +} + +void writeV3S32(u8 *data, v3s32 p) +{ + writeS32(&data[0], p.X); + writeS32(&data[4], p.Y); + writeS32(&data[8], p.Z); +} + +v3s32 readV3S32(u8 *data) +{ + v3s32 p; + p.X = readS32(&data[0]); + p.Y = readS32(&data[4]); + p.Z = readS32(&data[8]); + return p; +} + diff --git a/src/utility.h b/src/utility.h new file mode 100644 index 0000000..d2a711c --- /dev/null +++ b/src/utility.h @@ -0,0 +1,193 @@ +#ifndef UTILITY_HEADER +#define UTILITY_HEADER + +// This is actually only included for u8, u16, etc. +#include "common_irrlicht.h" + +void writeU32(u8 *data, u32 i); +void writeU16(u8 *data, u16 i); +void writeU8(u8 *data, u8 i); +u32 readU32(u8 *data); +u16 readU16(u8 *data); +u8 readU8(u8 *data); + +void writeV3S32(u8 *data, v3s32 p); +v3s32 readV3S32(u8 *data); + +// Inlines for signed variants of the above + +inline void writeS32(u8 *data, s32 i){ + writeU32(data, (u32)i); +} +inline s32 readS32(u8 *data){ + return (s32)readU32(data); +} + +inline void writeS16(u8 *data, s16 i){ + writeU16(data, (u16)i); +} +inline s16 readS16(u8 *data){ + return (s16)readU16(data); +} + +/* + None of these are used at the moment +*/ + +template +class SharedPtr +{ +public: + SharedPtr(T *t) + { + assert(t != NULL); + refcount = new int; + *refcount = 1; + ptr = t; + } + SharedPtr(SharedPtr &t) + { + *this = t; + } + ~SharedPtr() + { + (*refcount)--; + if(*refcount == 0) + { + delete refcount; + delete ptr; + } + } + SharedPtr & operator=(SharedPtr &t) + { + refcount = t.refcount; + (*refcount)++; + ptr = t.ptr; + return *this; + } + T* operator->() + { + return ptr; + } + T & operator*() + { + return *ptr; + } +private: + T *ptr; + int *refcount; +}; + +template +class Buffer +{ +public: + Buffer(unsigned int size) + { + m_size = size; + data = new T[size]; + } + Buffer(const Buffer &buffer) + { + m_size = buffer.m_size; + data = new T[buffer.m_size]; + memcpy(data, buffer.data, buffer.m_size); + } + Buffer(T *t, unsigned int size) + { + m_size = size; + data = new T[size]; + memcpy(data, t, size); + } + ~Buffer() + { + delete[] data; + } + T & operator[](unsigned int i) const + { + return data[i]; + } + T * operator*() const + { + return data; + } + unsigned int getSize() const + { + return m_size; + } +private: + T *data; + unsigned int m_size; +}; + +template +class SharedBuffer +{ +public: + SharedBuffer(unsigned int size) + { + m_size = size; + data = new T[size]; + refcount = new unsigned int (1); + } + SharedBuffer(const SharedBuffer &buffer) + { + m_size = buffer.m_size; + //data = new T[buffer.m_size]; + //memcpy(data, buffer.data, buffer.m_size); + data = buffer.data; + refcount = buffer.refcount; + (*refcount)++; + } + /* + Copies whole buffer + */ + SharedBuffer(T *t, unsigned int size) + { + m_size = size; + data = new T[size]; + memcpy(data, t, size); + refcount = new unsigned int (1); + } + /* + Copies whole buffer + */ + SharedBuffer(const Buffer &buffer) + { + m_size = buffer.m_size; + data = new T[buffer.getSize()]; + memcpy(data, *buffer, buffer.getSize()); + refcount = new unsigned int (1); + } + ~SharedBuffer() + { + (*refcount)--; + if(*refcount == 0) + delete[] data; + } + T & operator[](unsigned int i) const + { + return data[i]; + } + T * operator*() const + { + return data; + } + unsigned int getSize() const + { + return m_size; + } +private: + T *data; + unsigned int m_size; + unsigned int *refcount; +}; + +inline SharedBuffer SharedBufferFromString(const char *string) +{ + SharedBuffer b((u8*)string, strlen(string)+1); + return b; +} + +#endif +