Browse Source

Added package file download support to the network protocol.
Added commented out package download test code to TestScene.as.
Increased kNet's UDP send rate upper limit and aggressiveness in increasing the send rate.
Fixed being unable to do sparse seeks in files when writing.
Container library bugfixes.

Lasse Öörni 14 years ago
parent
commit
f70cc35815

+ 25 - 0
Bin/Data/Scripts/TestScene.as

@@ -8,6 +8,8 @@ float yaw = 0.0;
 float pitch = 0.0;
 int drawDebug = 0;
 
+Text@ downloadsText;
+
 void Start()
 {
     if (!engine.headless)
@@ -35,10 +37,15 @@ void Start()
     {
         network.StartServer(serverPort);
         SubscribeToEvent("ClientConnected", "HandleClientConnected");
+
+        //PackageFile@ packageFile = PackageFile(fileSystem.programDir + "Data.pak");
+        //cache.AddPackageFile(packageFile);
+        //testScene.AddRequiredPackageFile(packageFile);
     }
     if (startClient)
     {
         testScene.Clear();
+        //network.packageCacheDir = fileSystem.programDir;
         network.Connect(serverAddress, serverPort, testScene);
     }
 }
@@ -63,6 +70,11 @@ void InitUI()
     newCursor.style = uiStyle;
     newCursor.position = IntVector2(graphics.width / 2, graphics.height / 2);
     ui.cursor = newCursor;
+    
+    downloadsText = Text();
+    downloadsText.SetAlignment(HA_CENTER, VA_CENTER);
+    downloadsText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 20);
+    ui.root.AddChild(downloadsText);
 }
 
 void InitScene()
@@ -314,6 +326,19 @@ void HandleUpdate(StringHash eventType, VariantMap& eventData)
         else
             console.visible = false;
     }
+
+    // Update package download status
+    if (network.serverConnection !is null)
+    {
+        Connection@ connection = network.serverConnection;
+        if (connection.numDownloads > 0)
+        {
+            downloadsText.text = "Downloads: " + connection.numDownloads + " Current download: " +
+                connection.downloadName + " (" + connection.downloadProgress * 100.0 + "%)";
+        }
+        else if (!downloadsText.text.empty)
+            downloadsText.text = "";
+    }
 }
 
 void HandleKeyDown(StringHash eventType, VariantMap& eventData)

+ 4 - 2
Docs/Reference.dox

@@ -981,7 +981,7 @@ The attribute system also supports editing by providing human-readable names.
 
 The Network library provides reliable and unreliable UDP messaging using kNet. A server can be created that listens for incoming connections, and client connections can be made to the server. After connecting, code running on the server can assign the client into a scene to enable scene replication, provided that when connecting, the client also specified a blank scene that can be used.
 
-%Scene replication is one-directional: the server always has authority and sends scene updates to the client at a fixed update rate, by default 25 FPS. The client responds by sends controls updates (buttons, yaw and pitch + possible extra data) also at a fixed rate.
+%Scene replication is one-directional: the server always has authority and sends scene updates to the client at a fixed update rate, by default 25 FPS. The client responds by only sending controls updates (buttons, yaw and pitch + possible extra data) also at a fixed rate.
 
 Bidirectional communication between the server and the client can happen either using raw network messages, which are binary-serialized data, or remote events, which operate like ordinary events, but are processed on the receiving end only. Code on the server can send messages or remote events either to one client, all clients assigned into a particular scene, or to all connected clients. In contrast the client can only send messages or remote events to the server, not directly to other clients.
 
@@ -1003,6 +1003,8 @@ The CreateMode translates into two different node and component ID ranges - repl
 
 If the scene was originally loaded from a file on the server, the client will also load the scene from the same file first. In this case all predefined, static objects such as the world geometry should be defined as local nodes, so that they are not needlessly retransmitted through the network during the initial update, and do not exhaust the more limited replicated ID range.
 
+The server can be made to transmit needed resource \ref PackageFile "packages" to the client. This requires attaching the package files to the Scene by calling \ref Scene::AddRequiredPackageFile "AddRequiredPackageFile()". On the client, a cache directory for the packages must be chosen before receiving them is possible: see \ref Network::SetPackageCacheDir "SetPackageCacheDir()".
+
 There are some things to watch out for:
 
 - After connecting to a server, the client should not create, update or remove non-local nodes or components on its own. However, to create client-side special effects and such, the client can freely manipulate local nodes.
@@ -1029,7 +1031,7 @@ The Controls structure will be used to send controls information from the client
 
 \section Network_Messages Raw network messages
 
-All network messages have an integer ID. The first ID you can use for custom messages is 20 (lower ID's are either reserved for kNet's internal use or for the scene replication and remote event protocol.) Messages can be sent either unreliably or reliably, in-order or unordered. The data payload is simply raw binary data that can be crafted by using for example VectorBuffer.
+All network messages have an integer ID. The first ID you can use for custom messages is 22 (lower ID's are either reserved for kNet's or the %Network library's internal use.) Messages can be sent either unreliably or reliably, in-order or unordered. The data payload is simply raw binary data that can be crafted by using for example VectorBuffer.
 
 To send a message to a Connection, use its \ref Connection::SendMessage "SendMessage()" function. On the server, messages can also be broadcast to all client connections by calling the \ref Network::BroadcastMessage "BroadcastMessage()" function.
 

+ 5 - 1
Docs/ScriptAPI.dox

@@ -833,7 +833,7 @@ ResourceCache
 
 Methods:<br>
 - bool AddResourcePath(const String&)
-- void AddPackageFile(PackageFile@)
+- void AddPackageFile(PackageFile@, bool arg1 = false)
 - bool AddManualResource(Resource@)
 - void RemoveResourcePath(const String&)
 - void RemovePackageFile(PackageFile@, bool, bool)
@@ -3455,6 +3455,9 @@ Properties:<br>
 - bool sceneLoaded (readonly)
 - String address (readonly)
 - uint16 port (readonly)
+- uint numDownloads (readonly)
+- String& downloadName (readonly)
+- float downloadProgress (readonly)
 
 
 Network
@@ -3478,6 +3481,7 @@ Properties:<br>
 - ShortStringHash type (readonly)
 - String& typeName (readonly)
 - int updateFps
+- String& packageCacheDir
 - bool serverRunning (readonly)
 - Connection@ serverConnection (readonly)
 - Connection@[]@ clientConnections (readonly)

+ 2 - 2
Engine/Container/HashMap.h

@@ -127,11 +127,11 @@ public:
         /// Preincrement the pointer
         ConstIterator& operator ++ () { GotoNext(); return *this; }
         /// Postincrement the pointer
-        ConstIterator operator ++ (int) { Iterator it = *this; GotoNext(); return it; }
+        ConstIterator operator ++ (int) { ConstIterator it = *this; GotoNext(); return it; }
         /// Predecrement the pointer
         ConstIterator& operator -- () { GotoPrev(); return *this; }
         /// Postdecrement the pointer
-        ConstIterator operator -- (int) { Iterator it = *this; GotoPrev(); return it; }
+        ConstIterator operator -- (int) { ConstIterator it = *this; GotoPrev(); return it; }
         
         /// Point to the pair
         const KeyValue* operator -> () const { return &(static_cast<Node*>(ptr_))->pair_; }

+ 2 - 2
Engine/Container/HashSet.h

@@ -100,11 +100,11 @@ public:
         /// Preincrement the pointer
         ConstIterator& operator ++ () { GotoNext(); return *this; }
         /// Postincrement the pointer
-        ConstIterator operator ++ (int) { Iterator it = *this; GotoNext(); return it; }
+        ConstIterator operator ++ (int) { ConstIterator it = *this; GotoNext(); return it; }
         /// Predecrement the pointer
         ConstIterator& operator -- () { GotoPrev(); return *this; }
         /// Postdecrement the pointer
-        ConstIterator operator -- (int) { Iterator it = *this; GotoPrev(); return it; }
+        ConstIterator operator -- (int) { ConstIterator it = *this; GotoPrev(); return it; }
         
         /// Point to the key
         const T* operator -> () const { return &(static_cast<Node*>(ptr_))->key_; }

+ 2 - 1
Engine/Engine/Engine.cpp

@@ -160,7 +160,7 @@ bool Engine::Initialize(const String& windowTitle, const String& logName, const
     Log* log = GetSubsystem<Log>();
     log->Open(logName);
     
-    // Add default resource paths: CoreData package or directory, Data package or directory, system fonts directory
+    // Add default resource paths: CoreData package or directory, Data package or directory
     ResourceCache* cache = GetSubsystem<ResourceCache>();
     FileSystem* fileSystem = GetSubsystem<FileSystem>();
     String exePath = fileSystem->GetProgramDir();
@@ -172,6 +172,7 @@ bool Engine::Initialize(const String& windowTitle, const String& logName, const
     }
     else if (fileSystem->DirExists(exePath + "CoreData"))
         cache->AddResourcePath(exePath + "CoreData");
+    
     if (fileSystem->FileExists(exePath + "Data.pak"))
     {
         SharedPtr<PackageFile> package(new PackageFile(context_));

+ 5 - 0
Engine/Engine/NetworkAPI.cpp

@@ -85,6 +85,9 @@ static void RegisterConnection(asIScriptEngine* engine)
     engine->RegisterObjectMethod("Connection", "bool get_sceneLoaded() const", asMETHOD(Connection, IsSceneLoaded), asCALL_THISCALL);
     engine->RegisterObjectMethod("Connection", "String get_address() const", asMETHOD(Connection, GetAddress), asCALL_THISCALL);
     engine->RegisterObjectMethod("Connection", "uint16 get_port() const", asMETHOD(Connection, GetPort), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Connection", "uint get_numDownloads() const", asMETHOD(Connection, GetNumDownloads), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Connection", "const String& get_downloadName() const", asMETHOD(Connection, GetDownloadName), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Connection", "float get_downloadProgress() const", asMETHOD(Connection, GetDownloadProgress), asCALL_THISCALL);
     
     // Register Variant GetPtr() for Connection
     engine->RegisterObjectMethod("Variant", "Connection@+ GetConnection() const", asFUNCTION(GetVariantPtr<Connection>), asCALL_CDECL_OBJLAST);
@@ -171,6 +174,8 @@ void RegisterNetwork(asIScriptEngine* engine)
     engine->RegisterObjectMethod("Network", "bool CheckRemoteEvent(const String&in) const", asFUNCTION(NetworkCheckRemoteEvent), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("Network", "void set_updateFps(int)", asMETHOD(Network, SetUpdateFps), asCALL_THISCALL);
     engine->RegisterObjectMethod("Network", "int get_updateFps() const", asMETHOD(Network, GetUpdateFps), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Network", "void set_packageCacheDir(const String&in)", asMETHOD(Network, SetPackageCacheDir), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Network", "const String& get_packageCacheDir() const", asMETHOD(Network, GetPackageCacheDir), asCALL_THISCALL);
     engine->RegisterObjectMethod("Network", "bool get_serverRunning() const", asMETHOD(Network, IsServerRunning), asCALL_THISCALL);
     engine->RegisterObjectMethod("Network", "Connection@+ get_serverConnection() const", asMETHOD(Network, GetServerConnection), asCALL_THISCALL);
     engine->RegisterObjectMethod("Network", "Array<Connection@>@ get_clientConnections() const", asFUNCTION(NetworkGetClientConnections), asCALL_CDECL_OBJLAST);

+ 1 - 1
Engine/Engine/ResourceAPI.cpp

@@ -86,7 +86,7 @@ static void RegisterResourceCache(asIScriptEngine* engine)
 {
     RegisterObject<ResourceCache>(engine, "ResourceCache");
     engine->RegisterObjectMethod("ResourceCache", "bool AddResourcePath(const String&in)", asMETHOD(ResourceCache, AddResourcePath), asCALL_THISCALL);
-    engine->RegisterObjectMethod("ResourceCache", "void AddPackageFile(PackageFile@+)", asMETHOD(ResourceCache, AddPackageFile), asCALL_THISCALL);
+    engine->RegisterObjectMethod("ResourceCache", "void AddPackageFile(PackageFile@+, bool addAsFirst = false)", asMETHOD(ResourceCache, AddPackageFile), asCALL_THISCALL);
     engine->RegisterObjectMethod("ResourceCache", "bool AddManualResource(Resource@+)", asMETHOD(ResourceCache, AddManualResource), asCALL_THISCALL);
     engine->RegisterObjectMethod("ResourceCache", "void RemoveResourcePath(const String&in)", asMETHOD(ResourceCache, RemoveResourcePath), asCALL_THISCALL);
     engine->RegisterObjectMethod("ResourceCache", "void RemovePackageFile(PackageFile@+, bool, bool)", asMETHODPR(ResourceCache, RemovePackageFile, (PackageFile*, bool, bool), void), asCALL_THISCALL);

+ 2 - 1
Engine/IO/File.cpp

@@ -171,7 +171,8 @@ unsigned File::Read(void* dest, unsigned size)
 
 unsigned File::Seek(unsigned position)
 {
-    if (position > size_)
+    // Allow sparse seeks if writing
+    if (mode_ == FILE_READ && position > size_)
         position = size_;
     
     if (!handle_)

+ 354 - 40
Engine/Network/Connection.cpp

@@ -30,15 +30,27 @@
 #include "MemoryBuffer.h"
 #include "Network.h"
 #include "NetworkEvents.h"
+#include "PackageFile.h"
 #include "Profiler.h"
 #include "Protocol.h"
+#include "ResourceCache.h"
 #include "Scene.h"
 #include "SceneEvents.h"
+#include "StringUtils.h"
 
 #include <kNet.h>
 
 #include "DebugNew.h"
 
+static const String noName;
+
+PackageDownload::PackageDownload() :
+    totalFragments_(0),
+    checksum_(0),
+    initiated_(false)
+{
+}
+
 OBJECTTYPESTATIC(Connection);
 
 Connection::Connection(Context* context, bool isClient, kNet::SharedPtr<kNet::MessageConnection> connection) :
@@ -154,10 +166,19 @@ void Connection::SetScene(Scene* newScene)
     {
         sceneState_.Clear();
         
-        // When scene is assigned on the server, instruct the client to load it
-        /// \todo Download package(s) needed for the scene, if they do not exist already on the client
+        // When scene is assigned on the server, instruct the client to load it. This may require downloading packages
+        const Vector<SharedPtr<PackageFile> >& packages = scene_->GetRequiredPackageFiles();
+        unsigned numPackages = packages.Size();
         msg_.Clear();
         msg_.WriteString(scene_->GetFileName());
+        msg_.WriteVLE(numPackages);
+        for (unsigned i = 0; i < numPackages; ++i)
+        {
+            PackageFile* package = packages[i];
+            msg_.WriteString(GetFileNameAndExtension(package->GetName()));
+            msg_.WriteUInt(package->GetTotalSize());
+            msg_.WriteUInt(package->GetChecksum());
+        }
         SendMessage(MSG_LOADSCENE, true, true, msg_);
     }
     else
@@ -316,43 +337,89 @@ void Connection::ProcessLoadScene(int msgID, MemoryBuffer& msg)
         return;
     }
     
-    String fileName = msg.ReadString();
+    // Store the scene file name we need to eventually load
+    sceneFileName_ = msg.ReadString();
     
-    // Make sure there is no existing async loading, and clear previous pending latest data if any
-    scene_->StopAsyncLoading();
+    // Clear previous scene content, pending latest data, and package downloads if any
+    scene_->Clear();
     nodeLatestData_.Clear();
     componentLatestData_.Clear();
+    downloads_.Clear();
     
-    if (fileName.Empty())
+    // In case we have joined other scenes in this session, remove first all downloaded package files from the resource system
+    // to prevent resource conflicts
+    const String& packageCacheDir = GetSubsystem<Network>()->GetPackageCacheDir();
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    Vector<SharedPtr<PackageFile> > packages = cache->GetPackageFiles();
+    for (unsigned i = 0; i < packages.Size(); ++i)
     {
-        scene_->Clear();
-        
-        // If filename is empty, can send the scene loaded reply immediately
-        VectorBuffer replyMsg;
-        replyMsg.WriteUInt(scene_->GetChecksum());
-        SendMessage(MSG_SCENELOADED, true, true, replyMsg);
+        PackageFile* package = packages[i];
+        if (!package->GetName().Find(packageCacheDir))
+            cache->RemovePackageFile(package, true);
     }
-    else
+    
+    // Now check which packages we have in the resource cache or in the download cache, and which we need to download
+    unsigned numPackages = msg.ReadVLE();
+    packages = cache->GetPackageFiles(); // Refresh resource cache's package list after possible removals
+    Vector<String> downloadedPackages;
+    if (!packageCacheDir.Empty())
+        GetSubsystem<FileSystem>()->ScanDir(downloadedPackages, packageCacheDir, "*.*", SCAN_FILES, false);
+    
+    for (unsigned i = 0; i < numPackages; ++i)
     {
-        // Otherwise start the async loading process
-        String extension = GetExtension(fileName);
-        SharedPtr<File> file(new File(context_, fileName));
-        bool success;
+        String name = msg.ReadString();
+        unsigned fileSize = msg.ReadUInt();
+        unsigned checksum = msg.ReadUInt();
+        String checksumString = ToStringHex(checksum);
+        bool found = false;
         
-        if (extension == ".xml")
-            success = scene_->LoadAsyncXML(file);
-        else
-            success = scene_->LoadAsync(file);
+        // Check first the resource cache
+        for (unsigned j = 0; j < packages.Size(); ++j)
+        {
+            PackageFile* package = packages[j];
+            if (!GetFileNameAndExtension(package->GetName()).Compare(name, false) && package->GetTotalSize() == fileSize &&
+                package->GetChecksum() == checksum)
+            {
+                found = true;
+                break;
+            }
+        }
         
-        if (!success)
+        // Then the download cache
+        for (unsigned j = 0; j < downloadedPackages.Size(); ++j)
         {
-            using namespace NetworkSceneLoadFailed;
-            
-            VariantMap eventData;
-            eventData[P_CONNECTION] = (void*)this;
-            SendEvent(E_NETWORKSCENELOADFAILED, eventData);
+            const String& fileName = downloadedPackages[j];
+            if (!fileName.Find(checksumString) && !fileName.Substring(9).Compare(name, false))
+            {
+                // Name matches. Check filesize and actual checksum to be sure
+                SharedPtr<PackageFile> newPackage(new PackageFile(context_, packageCacheDir + fileName));
+                if (newPackage->GetTotalSize() == fileSize && newPackage->GetChecksum() == checksum)
+                {
+                    // Add the package to the resource cache, as we will need it to load the scene
+                    cache->AddPackageFile(newPackage, true);
+                    found = true;
+                    break;
+                }
+            }
+        }
+        
+        // Need to request a download
+        if (!found)
+        {
+            if (!packageCacheDir.Empty())
+                RequestPackage(name, fileSize, checksum);
+            else
+            {
+                LOGERROR("Can not download required packages, as no package cache path is set");
+                OnSceneLoadFailed();
+                return;
+            }
         }
     }
+    
+    // If no downloads were queued, can load the scene directly
+    if (downloads_.Empty())
+        OnPackagesReady();
 }
 
 void Connection::ProcessSceneChecksumError(int msgID, MemoryBuffer& msg)
@@ -363,11 +430,7 @@ void Connection::ProcessSceneChecksumError(int msgID, MemoryBuffer& msg)
         return;
     }
     
-    using namespace NetworkSceneLoadFailed;
-    
-    VariantMap eventData;
-    eventData[P_CONNECTION] = (void*)this;
-    SendEvent(E_NETWORKSCENELOADFAILED, eventData);
+    OnSceneLoadFailed();
 }
 
 void Connection::ProcessSceneUpdate(int msgID, MemoryBuffer& msg)
@@ -576,6 +639,157 @@ void Connection::ProcessSceneUpdate(int msgID, MemoryBuffer& msg)
     }
 }
 
+void Connection::ProcessPackageDownload(int msgID, MemoryBuffer& msg)
+{
+    switch (msgID)
+    {
+    case MSG_REQUESTPACKAGE:
+        if (!IsClient())
+        {
+            LOGWARNING("Received unexpected RequestPackage message from server");
+            return;
+        }
+        else
+        {
+            String name = msg.ReadString();
+            
+            if (!scene_)
+            {
+                LOGWARNING("Received a RequestPackage message without an assigned scene from client " + ToString());
+                return;
+            }
+            
+            // The package must be one of those required by the scene
+            const Vector<SharedPtr<PackageFile> >& packages = scene_->GetRequiredPackageFiles();
+            for (unsigned i = 0; i < packages.Size(); ++i)
+            {
+                PackageFile* package = packages[i];
+                String packageFullName = package->GetName();
+                if (!GetFileNameAndExtension(packageFullName).Compare(name, false))
+                {
+                    SharedPtr<File> file(new File(context_, packageFullName));
+                    if (!file->IsOpen())
+                    {
+                        LOGERROR("Failed to transmit package file " + name);
+                        SendPackageError(name);
+                        return;
+                    }
+                    
+                    LOGINFO("Transmitting package file " + name + " to client " + ToString());
+                    
+                    StringHash nameHash(name);
+                    unsigned totalFragments = (file->GetSize() + PACKAGE_FRAGMENT_SIZE - 1) / PACKAGE_FRAGMENT_SIZE;
+                    unsigned char buffer[PACKAGE_FRAGMENT_SIZE];
+                    
+                    // Now simply read the file fragments and queue them
+                    for (unsigned i = 0; i < totalFragments; ++i)
+                    {
+                        unsigned fragmentSize = Min((int)(file->GetSize() - file->GetPosition()), (int)PACKAGE_FRAGMENT_SIZE);
+                        file->Read(buffer, fragmentSize);
+                        
+                        msg_.Clear();
+                        msg_.WriteStringHash(nameHash);
+                        msg_.WriteVLE(i);
+                        msg_.Write(buffer, fragmentSize);
+                        SendMessage(MSG_PACKAGEDATA, true, false, msg_);
+                    }
+                    
+                    return;
+                }
+            }
+            
+            LOGERROR("Client requested a nonexisting package file " + name);
+            // Send the name hash only to indicate a failed download
+            SendPackageError(name);
+            return;
+        }
+        break;
+        
+    case MSG_PACKAGEDATA:
+        if (IsClient())
+        {
+            LOGWARNING("Received unexpected PackageData message from client");
+            return;
+        }
+        else
+        {
+            StringHash nameHash = msg.ReadStringHash();
+            
+            Map<StringHash, PackageDownload>::Iterator i = downloads_.Find(nameHash);
+            // In case of being unable to create the package file into the cache, we will still receive all data from the server.
+            // Simply disregard it
+            if (i == downloads_.End())
+                return;
+            
+            PackageDownload& download = i->second_;
+            
+            // If no further data, this is an error reply
+            if (msg.IsEof())
+            {
+                LOGERROR("Download of package " + download.name_ + " failed");
+                OnPackageDownloadFailed();
+                return;
+            }
+            
+            // If file has not yet been opened, try to open now. Prepend the checksum to the filename to allow multiple versions
+            if (!download.file_)
+            {
+                download.file_ = new File(context_, GetSubsystem<Network>()->GetPackageCacheDir() + ToStringHex(download.checksum_) + "_" + download.name_, FILE_WRITE);
+                if (!download.file_->IsOpen())
+                {
+                    LOGERROR("Download of package " + download.name_ + " failed");
+                    OnPackageDownloadFailed();
+                    return;
+                }
+            }
+            
+            // Write the fragment data to the proper index
+            unsigned char buffer[PACKAGE_FRAGMENT_SIZE];
+            unsigned index = msg.ReadVLE();
+            unsigned fragmentSize = msg.GetSize() - msg.GetPosition();
+            
+            msg.Read(buffer, fragmentSize);
+            download.file_->Seek(index * PACKAGE_FRAGMENT_SIZE);
+            download.file_->Write(buffer, fragmentSize);
+            download.receivedFragments_.Insert(index);
+            
+            // Check if all fragments received
+            if (download.receivedFragments_.Size() > download.totalFragments_)
+            {
+                LOGERROR("Received extra fragments for package " + download.name_);
+                OnPackageDownloadFailed();
+                return;
+            }
+            
+            if (download.receivedFragments_.Size() == download.totalFragments_)
+            {
+                LOGINFO("Package " + download.name_ + " downloaded successfully");
+                
+                // Instantiate the package and add to the resource system, as we will need it to load the scene
+                download.file_->Close();
+                SharedPtr<PackageFile> newPackage(new PackageFile(context_, download.file_->GetName()));
+                GetSubsystem<ResourceCache>()->AddPackageFile(newPackage, true);
+                
+                // Then start the next download if there are more
+                downloads_.Erase(i);
+                if (downloads_.Empty())
+                    OnPackagesReady();
+                else
+                {
+                    PackageDownload& nextDownload = downloads_.Begin()->second_;
+                    
+                    LOGINFO("Requesting package " + nextDownload.name_ + " from server");
+                    msg_.Clear();
+                    msg_.WriteString(nextDownload.name_);
+                    SendMessage(MSG_REQUESTPACKAGE, true, true, msg_);
+                    nextDownload.initiated_ = true;
+                }
+            }
+        }
+        break;
+    }
+}
+
 void Connection::ProcessIdentity(int msgID, MemoryBuffer& msg)
 {
     if (!IsClient())
@@ -632,14 +846,9 @@ void Connection::ProcessSceneLoaded(int msgID, MemoryBuffer& msg)
     
     if (checksum != scene_->GetChecksum())
     {
-        VectorBuffer replyMsg;
-        SendMessage(MSG_SCENECHECKSUMERROR, true, true, replyMsg);
-        
-        using namespace NetworkSceneLoadFailed;
-        
-        VariantMap eventData;
-        eventData[P_CONNECTION] = (void*)this;
-        SendEvent(E_NETWORKSCENELOADFAILED, eventData);
+        msg_.Clear();
+        SendMessage(MSG_SCENECHECKSUMERROR, true, true, msg_);
+        OnSceneLoadFailed();
     }
     else
     {
@@ -727,6 +936,34 @@ String Connection::ToString() const
     return GetAddress() + ":" + String(GetPort());
 }
 
+unsigned Connection::GetNumDownloads() const
+{
+    return downloads_.Size();
+}
+
+const String& Connection::GetDownloadName() const
+{
+    for (Map<StringHash, PackageDownload>::ConstIterator i = downloads_.Begin(); i != downloads_.End(); ++i)
+    {
+        if (i->second_.initiated_)
+            return i->second_.name_;
+    }
+    return noName;
+}
+
+float Connection::GetDownloadProgress() const
+{
+    for (Map<StringHash, PackageDownload>::ConstIterator i = downloads_.Begin(); i != downloads_.End(); ++i)
+    {
+        if (i->second_.initiated_)
+        {
+            return i->second_.totalFragments_ ? (float)i->second_.receivedFragments_.Size() / (float)i->second_.totalFragments_ :
+                0.0f;
+        }
+    }
+    return 1.0f;
+}
+
 void Connection::HandleAsyncLoadFinished(StringHash eventType, VariantMap& eventData)
 {
     VectorBuffer msg;
@@ -923,3 +1160,80 @@ void Connection::ProcessExistingNode(Node* node)
         }
     }
 }
+
+void Connection::RequestPackage(const String& name, unsigned fileSize, unsigned checksum)
+{
+    StringHash nameHash(name);
+    if (downloads_.Contains(nameHash))
+        return; // Download already exists
+    
+    PackageDownload& download = downloads_[nameHash];
+    download.name_ = name;
+    download.totalFragments_ = (fileSize + PACKAGE_FRAGMENT_SIZE - 1) / PACKAGE_FRAGMENT_SIZE;
+    download.checksum_ = checksum;
+    
+    // Start download now only if no existing downloads, else wait for the existing ones to finish
+    if (downloads_.Size() == 1)
+    {
+        LOGINFO("Requesting package " + name + " from server");
+        msg_.Clear();
+        msg_.WriteString(name);
+        SendMessage(MSG_REQUESTPACKAGE, true, true, msg_);
+        download.initiated_ = true;
+    }
+}
+
+void Connection::SendPackageError(const String& name)
+{
+    msg_.Clear();
+    msg_.WriteStringHash(StringHash(name));
+    SendMessage(MSG_PACKAGEDATA, true, false, msg_);
+}
+
+void Connection::OnSceneLoadFailed()
+{
+    using namespace NetworkSceneLoadFailed;
+    
+    VariantMap eventData;
+    eventData[P_CONNECTION] = (void*)this;
+    SendEvent(E_NETWORKSCENELOADFAILED, eventData);
+    return;
+}
+
+void Connection::OnPackageDownloadFailed()
+{
+    // As one package failed, we can not join the scene in any case. Clear the downloads
+    downloads_.Clear();
+    OnSceneLoadFailed();
+}
+
+void Connection::OnPackagesReady()
+{
+    if (!scene_)
+        return;
+    
+    if (sceneFileName_.Empty())
+    {
+        scene_->Clear();
+        
+        // If filename is empty, can send the scene loaded reply immediately
+        msg_.Clear();
+        msg_.WriteUInt(scene_->GetChecksum());
+        SendMessage(MSG_SCENELOADED, true, true, msg_);
+    }
+    else
+    {
+        // Otherwise start the async loading process
+        String extension = GetExtension(sceneFileName_);
+        SharedPtr<File> file(new File(context_, sceneFileName_));
+        bool success;
+        
+        if (extension == ".xml")
+            success = scene_->LoadAsyncXML(file);
+        else
+            success = scene_->LoadAsync(file);
+        
+        if (!success)
+            OnSceneLoadFailed();
+    }
+}

+ 43 - 3
Engine/Network/Connection.h

@@ -36,14 +36,12 @@
 #undef SendMessage
 #endif
 
+class File;
 class MemoryBuffer;
 class Node;
 class Scene;
 class Serializable;
 
-/// Message priority for kNet. For now all messages have same priority
-static const int DEFAULT_MSG_PRIORITY = 100;
-
 /// Queued remote event
 struct RemoteEvent
 {
@@ -57,6 +55,26 @@ struct RemoteEvent
     bool inOrder_;
 };
 
+/// Package file download
+struct PackageDownload
+{
+    /// Construct with defaults
+    PackageDownload();
+    
+    /// Destination file that is used to write the data
+    SharedPtr<File> file_;
+    /// Already received fragments
+    HashSet<unsigned> receivedFragments_;
+    /// Package name
+    String name_;
+    /// Total number of fragments
+    unsigned totalFragments_;
+    /// Checksum
+    unsigned checksum_;
+    /// Download initiated flag
+    bool initiated_;
+};
+
 /// Connection in a networked scene
 class Connection : public Object
 {
@@ -104,6 +122,8 @@ public:
     void ProcessSceneChecksumError(int msgID, MemoryBuffer& msg);
     /// Process a scene update message from the server. Called by Network
     void ProcessSceneUpdate(int msgID, MemoryBuffer& msg);
+    /// Process package download related messages. Called by Network
+    void ProcessPackageDownload(int msgID, MemoryBuffer& msg);
     /// Process an Identity message from the client. Called by Network
     void ProcessIdentity(int msgID, MemoryBuffer& msg);
     /// Process a Controls message from the client. Called by Network
@@ -137,6 +157,12 @@ public:
     unsigned short GetPort() const;
     /// Return an address:port string
     String ToString() const;
+    /// Return number of package downloads remaining
+    unsigned GetNumDownloads() const;
+    /// Return name of current package download, or empty if no downloads
+    const String& GetDownloadName() const;
+    /// Return progress of current package download, or 1.0 if no downloads
+    float GetDownloadProgress() const;
     
 private:
     /// Handle scene loaded event
@@ -147,6 +173,16 @@ private:
     void ProcessNewNode(Node* node);
     /// Process a node that the client has already received
     void ProcessExistingNode(Node* node);
+    /// Initiate a package download
+    void RequestPackage(const String& name, unsigned fileSize, unsigned checksum);
+    /// Send an error reply for a package download
+    void SendPackageError(const String& name);
+    /// Handle scene load failure on the server or client
+    void OnSceneLoadFailed();
+    /// Handle a package download failure on the client
+    void OnPackageDownloadFailed();
+    /// Handle all packages loaded successfully. Also called directly on MSG_LOADSCENE if there are none
+    void OnPackagesReady();
     
     /// kNet message connection
     kNet::SharedPtr<kNet::MessageConnection> connection_;
@@ -170,6 +206,10 @@ private:
     HashSet<Node*> processedNodes_;
     /// Preallocated variants of correct type per networked object class
     Map<ShortStringHash, Vector<Variant> > classCurrentState_;
+    /// Waiting or ongoing package file downloads
+    Map<StringHash, PackageDownload> downloads_;
+    /// Scene file to load once all packages (if any) have been downloaded
+    String sceneFileName_;
     /// Reused message buffer
     VectorBuffer msg_;
     /// Current controls

+ 23 - 12
Engine/Network/Network.cpp

@@ -24,6 +24,7 @@
 #include "Precompiled.h"
 #include "Context.h"
 #include "CoreEvents.h"
+#include "FileSystem.h"
 #include "Log.h"
 #include "MemoryBuffer.h"
 #include "Network.h"
@@ -74,6 +75,23 @@ void Network::HandleMessage(kNet::MessageConnection* source, kNet::message_id_t
         
         switch (id)
         {
+        case MSG_IDENTITY:
+            connection->ProcessIdentity(id, msg);
+            return;
+        
+        case MSG_CONTROLS:
+            connection->ProcessControls(id, msg);
+            return;
+            
+        case MSG_SCENELOADED:
+            connection->ProcessSceneLoaded(id, msg);
+            return;
+            
+        case MSG_REQUESTPACKAGE:
+        case MSG_PACKAGEDATA:
+            connection->ProcessPackageDownload(id, msg);
+            return;
+            
         case MSG_LOADSCENE:
             connection->ProcessLoadScene(id, msg);
             return;
@@ -93,18 +111,6 @@ void Network::HandleMessage(kNet::MessageConnection* source, kNet::message_id_t
             connection->ProcessSceneUpdate(id, msg);
             return;
             
-        case MSG_IDENTITY:
-            connection->ProcessIdentity(id, msg);
-            return;
-        
-        case MSG_CONTROLS:
-            connection->ProcessControls(id, msg);
-            return;
-            
-        case MSG_SCENELOADED:
-            connection->ProcessSceneLoaded(id, msg);
-            return;
-            
         case MSG_REMOTEEVENT:
         case MSG_REMOTENODEEVENT:
             connection->ProcessRemoteEvent(id, msg);
@@ -344,6 +350,11 @@ void Network::UnregisterAllRemoteEvents()
     allowedRemoteEvents_.Clear();
 }
 
+void Network::SetPackageCacheDir(const String& path)
+{
+    packageCacheDir_ = AddTrailingSlash(path);
+}
+
 Connection* Network::GetConnection(kNet::MessageConnection* connection) const
 {
     Map<kNet::MessageConnection*, SharedPtr<Connection> >::ConstIterator i = clientConnections_.Find(connection);

+ 6 - 0
Engine/Network/Network.h

@@ -83,6 +83,8 @@ public:
     void UnregisterRemoteEvent(StringHash eventType);
     /// Unregister all remote events. This results in all being allowed
     void UnregisterAllRemoteEvents();
+    /// Set the package download cache path
+    void SetPackageCacheDir(const String& path);
     
     /// Return network update FPS
     int GetUpdateFps() const { return updateFps_; }
@@ -96,6 +98,8 @@ public:
     bool IsServerRunning() const;
     /// Return whether a remote event is allowed to be sent and received. If no events are registered, all are allowed
     bool CheckRemoteEvent(StringHash eventType) const;
+    /// Return the package download cache path
+    const String& GetPackageCacheDir() const { return packageCacheDir_; }
     
     /// Update connections. Called by HandleBeginFrame
     void Update(float timeStep);
@@ -122,4 +126,6 @@ private:
     float updateInterval_;
     /// Network update time accumulator
     float updateAcc_;
+    /// Package cache path
+    String packageCacheDir_;
 };

+ 20 - 12
Engine/Network/Protocol.h

@@ -29,32 +29,40 @@ static const int MSG_IDENTITY = 0x5;
 static const int MSG_CONTROLS = 0x6;
 // Client->server: scene has been loaded and client is ready to proceed
 static const int MSG_SCENELOADED = 0x7;
+// Client->server: request a package file
+static const int MSG_REQUESTPACKAGE = 0x8;
 
+// Server->client: package file data fragment
+static const int MSG_PACKAGEDATA = 0x9;
 // Server->client: load new scene. In case of empty filename the client should just empty the scene
-static const int MSG_LOADSCENE = 0x8;
+static const int MSG_LOADSCENE = 0xa;
 // Server->client: wrong scene checksum, can not participate
-static const int MSG_SCENECHECKSUMERROR = 0x9;
+static const int MSG_SCENECHECKSUMERROR = 0xb;
 // Server->client: create new node
-static const int MSG_CREATENODE = 0xa;
+static const int MSG_CREATENODE = 0xc;
 // Server->client: node delta update
-static const int MSG_NODEDELTAUPDATE = 0xb;
+static const int MSG_NODEDELTAUPDATE = 0xd;
 // Server->client: node latest data update
-static const int MSG_NODELATESTDATA = 0xc;
+static const int MSG_NODELATESTDATA = 0xe;
 // Server->client: remove node
-static const int MSG_REMOVENODE = 0xd;
+static const int MSG_REMOVENODE = 0xf;
 // Server->client: create new component
-static const int MSG_CREATECOMPONENT = 0xe;
+static const int MSG_CREATECOMPONENT = 0x10;
 // Server->client: component delta update
-static const int MSG_COMPONENTDELTAUPDATE = 0xf;
+static const int MSG_COMPONENTDELTAUPDATE = 0x11;
 // Server->client: component latest data update
-static const int MSG_COMPONENTLATESTDATA = 0x10;
+static const int MSG_COMPONENTLATESTDATA = 0x12;
 // Server->client: remove component
-static const int MSG_REMOVECOMPONENT = 0x11;
+static const int MSG_REMOVECOMPONENT = 0x13;
 
 // Client->server and server->client: remote event
-static const int MSG_REMOTEEVENT = 0x12;
+static const int MSG_REMOTEEVENT = 0x14;
 // Client->server and server->client: remote node event
-static const int MSG_REMOTENODEEVENT = 0x13;
+static const int MSG_REMOTENODEEVENT = 0x15;
 
+// Fixed message priority for kNet
+static const int DEFAULT_MSG_PRIORITY = 100;
 // Fixed content ID for client controls update
 static const unsigned CONTROLS_CONTENT_ID = 1;
+// Package file fragment size
+static const unsigned PACKAGE_FRAGMENT_SIZE = 1024;

+ 7 - 3
Engine/Scene/Scene.cpp

@@ -256,6 +256,7 @@ void Scene::StopAsyncLoading()
 
 void Scene::Clear()
 {
+    StopAsyncLoading();
     RemoveAllChildren();
     RemoveAllComponents();
     fileName_ = String();
@@ -277,10 +278,13 @@ void Scene::SetSnapThreshold(float threshold)
     snapThreshold_ = Max(threshold, 0.0f);
 }
 
-void Scene::AddRequiredPackageFile(PackageFile* file)
+void Scene::AddRequiredPackageFile(PackageFile* package)
 {
-    if (file)
-        requiredPackageFiles_.Push(SharedPtr<PackageFile>(file));
+    // Do not add packages that failed to load
+    if (!package || !package->GetNumFiles())
+        return;
+    
+    requiredPackageFiles_.Push(SharedPtr<PackageFile>(package));
 }
 
 void Scene::ClearRequiredPackageFiles()

+ 1 - 1
Engine/Scene/Scene.h

@@ -94,7 +94,7 @@ public:
     /// Set motion smoothing snap threshold
     void SetSnapThreshold(float threshold);
     /// Add a required package file for networking. To be called on the server
-    void AddRequiredPackageFile(PackageFile* file);
+    void AddRequiredPackageFile(PackageFile* package);
     /// Clear required package files
     void ClearRequiredPackageFiles();
     /// Reset specific owner reference from nodes on disconnect

+ 2 - 2
ThirdParty/kNet/src/UDPMessageConnection.cpp

@@ -268,8 +268,8 @@ void UDPMessageConnection::HandleFlowControl()
 	AssertInWorkerThreadContext();
 
 	// In packets/second.
-	const float totalEstimatedBandwidth = 100; ///\todo Make this estimation dynamic as in UDT or similar.
-	const float additiveIncreaseAggressiveness = 5e-2f;
+	const float totalEstimatedBandwidth = 1000; ///\todo Make this estimation dynamic as in UDT or similar.
+	const float additiveIncreaseAggressiveness = 0.1f;
 
 	const tick_t frameLength = Clock::TicksPerSec() / 100; // in ticks
 	// Additively increase the outbound send rate.