Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Downloading game content  (Read 4428 times)

0 Members and 1 Guest are viewing this topic.

ChronicRat

  • Sr. Member
  • ****
  • Posts: 327
  • C++ programmer
    • View Profile
    • My blog
Downloading game content
« on: April 23, 2014, 05:29:45 pm »
I have very little network programming experience. So, my question is more abstract than practical. I want that my game to download missing content from my site and to show user progress of downloading. Which method is the best for such task?

select_this

  • Full Member
  • ***
  • Posts: 130
  • Current mood: just ate a pinecone
    • View Profile
    • darrenferrie.com
Re: Downloading game content
« Reply #1 on: April 23, 2014, 06:23:57 pm »
The client-side application will need to know how large each file is before it can give any sort of estimate as to how much percentage-wise has been received, so I'd make the server provide meta information about the file to be received (you could also use this information to validate the file once it has been received, for example a checksum) before actually initiating the transfer. You could then count the number of bytes in each packet received by the client and add them to the total received so far.

As for actually sending / receiving the data, there's a number of avenues available to you. Most likely the simplest would be sending the bytes over a TCP socket, as you would with any other data. HTTP or FTP would also be an option (though I'd struggle to come up with a good argument to use FTP over other avenues).
« Last Edit: April 23, 2014, 06:42:33 pm by ghostmemory »
Follow me on Twitter, why don'tcha? @select_this

ChronicRat

  • Sr. Member
  • ****
  • Posts: 327
  • C++ programmer
    • View Profile
    • My blog
Re: Downloading game content
« Reply #2 on: April 23, 2014, 06:39:46 pm »
Thank you!  :)

ChronicRat

  • Sr. Member
  • ****
  • Posts: 327
  • C++ programmer
    • View Profile
    • My blog
Re: Downloading game content
« Reply #3 on: April 24, 2014, 07:34:06 pm »
Reinventing the wheel... =) This is just test app.

bool GetLine(sf::TcpSocket& socket, std::string& line)
{
        line.clear();
        char ch;
        size_t received = 0;

        while (true)
        {
                if (socket.receive(&ch, 1, received) != sf::Socket::Done || received == 0)
                {
                        return false;
                }

               
                if (ch != '\r' && ch != '\n')
                {
                        line += ch;
                }

                if (ch == '\n')
                {
                        break;
                }
        }

        return true;
}



size_t GetAnswer(sf::TcpSocket& socket)
{
        std::string line;
        size_t res = 0;

        do
        {
                if (!GetLine(socket, line))
                {
                        return (size_t) -1;
                }

                if (line.empty())
                {
                        break;
                }

                printf("%s\n", line.c_str());
                size_t pos = line.find_first_of(':');

                if (pos != std::string::npos)
                {
                        std::string field = line.substr(0, pos);
                        pos += 2;

                        if (field == "Content-Length" && pos < line.length())
                        {
                                std::string value = line.substr(pos);
                                char* end;
                                auto val = strtoul(value.c_str(), &end, 10);
                               
                                if (errno != ERANGE)
                                {
                                        res = val;
                                }
                        }
                }
        } while (!line.empty());

        return res;
}



unsigned int CalcSpeed(const sf::Clock& clock, size_t down)
{
        auto elapsed = clock.getElapsedTime();
        auto seconds = elapsed.asSeconds();
        return seconds > 0 ? down / elapsed.asSeconds() : 0;
}



int _tmain(int argc, _TCHAR* argv[])
{
        sf::TcpSocket socket;
        sf::Socket::Status status = socket.connect("www.site.ru", 80);

        const size_t size = 4096;
        char* buf[size];


        if (status == sf::Socket::Done)
        {
                std::FILE* file = std::fopen("test.pak", "wb");
               
                if (!file)
                {
                        printf("Can't open file for writing\n");
                        return 1;
                }

                std::string req =
                "GET /somewhere/file.zip k HTTP/1.1\r\n"
                "Host: site.ru\r\n"
                "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0\r\n"
                "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
                "Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3\r\n"
                "Accept-Encoding: chunked\r\n"
                "Vary: Accept-Encoding\r\n"
                "Content-Type: application/octet-stream\r\n"
                "Connection: keep-alive\r\n\r\n";
               
                if (socket.send(req.c_str(), req.length()) == sf::Socket::Done)
                {
                        size_t received = 0;
                        size_t total = 0;
                       
                        size_t target = GetAnswer(socket);

                        if (target != (size_t) -1 && target > 0)
                        {
                                sf::Clock clock;

                                while (socket.receive(buf, size, received) == sf::Socket::Done)
                                {
                                        if (received > 0)
                                        {
                                                total += received;
                                                std::fwrite(buf, 1, received, file);
                                                int percents = 100 * ((float) total / (float) target);
                                                auto speed = CalcSpeed(clock, total);
                                                printf("Loaded %.3d%% %dKb\tav. speed: %dKb/s      \r", percents, total / 1024, speed / 1024);
                                        }
                                }

                                std::fclose(file);
                                printf("\n");
                        }
                }
               
                socket.disconnect();
        }


        getchar();
        return 0;
}
« Last Edit: April 24, 2014, 07:53:40 pm by ChronicRat »

eigenbom

  • Full Member
  • ***
  • Posts: 228
    • View Profile
Re: Downloading game content
« Reply #4 on: April 25, 2014, 12:49:04 am »
I would make a loader program that has a little http client in it, checks if all files are there, downloads them, then boots the actual game.

ChronicRat

  • Sr. Member
  • ****
  • Posts: 327
  • C++ programmer
    • View Profile
    • My blog
Re: Downloading game content
« Reply #5 on: April 25, 2014, 03:56:17 pm »
That's what I'm doing. =)

eigenbom

  • Full Member
  • ***
  • Posts: 228
    • View Profile
Re: Downloading game content
« Reply #6 on: April 27, 2014, 02:40:54 am »
Oh right cool. You could consider libcurl too if you like, or even a little qt app. Not sure how easy a launcher and downloader will be with sfml.

ChronicRat

  • Sr. Member
  • ****
  • Posts: 327
  • C++ programmer
    • View Profile
    • My blog
Re: Downloading game content
« Reply #7 on: April 27, 2014, 07:32:11 am »
I think about this:
<?php
$gameId = intval( $_GET['gameId'] );
$version = intval( $_GET['version'] );

if ( $gameId && $version )
{
        $files = array();
        $rootDir = $_SERVER['DOCUMENT_ROOT'] . '/res_files';
        $scanDir = sprintf( '%s/%d/%d', $rootDir, $gameId, $version );
        $dirContent = scandir($scanDir);

        if ($dirContent)
        {      
                foreach ( $dirContent as $entry )
                {
                        if ( !is_file( sprintf( '%s/%s', $scanDir, $entry ) ) )
                        {
                                continue;
                        }
                        $files[$entry] = filesize( sprintf( '%s/%s', $scanDir, $entry ) );
                }
                echo json_encode ( $files );
        }
}

?>
 
I checked, it perfectly works - returs files list and size for each file. But now i have other tasks to do now, so i'll return to this question later. Here my code for example, it can download one file, but getting several files is not complete.

#ifndef __GIPE_HTTP_DOWNLOAD_H__
#define __GIPE_HTTP_DOWNLOAD_H__



#define HTTP_OK 200



class HttpAnswer final
{
        typedef std::map<std::string, std::string> StringMap;

public:
        HttpAnswer() = default;

        inline bool HasField(const std::string& name) const
        {
                return mFields.find(name) != mFields.end();
        }

        const std::string& GetValue(const std::string& name) const;
        bool GetAsUnsigned(const std::string& name, size_t& res) const;
        const std::string& GetVersion() const { return mVersion; }

        int ReceiveAnswer(sf::TcpSocket& socket);

private:

        int ParseFirstLine(const std::string& line);
        bool GetLine(sf::TcpSocket& socket, std::string& line);

        StringMap               mFields;
        std::string             mVersion;
};



class HttpDownload final
{
public:

        struct UpdateInfo
        {
                float           mCurrentComplete = -1.0f; // if less than zero - is unknown
                unsigned        mSpeed = 0;
        };

        typedef std::function<void(const std::string& filename,
                                        const UpdateInfo& info)> DownloadUpdateCallback;


        HttpDownload() = default;
        ~HttpDownload();

        bool Connect(const std::string& site);
        bool IsOk() const { return mIsOk; }

        template<typename T>
        void SetUpdateCallback(T* owner,void (T::*func)(const std::string& filename,
                                                const UpdateInfo& info))
        {
                mUpdateCallback = std::bind(func, owner, std::placeholders::_1,
                        std::placeholders::_2);
        }

        void SetUpdateCallback(void (*func)(const std::string& filename,
                                                const UpdateInfo& info))
        {
                mUpdateCallback = std::bind(func, std::placeholders::_1,
                        std::placeholders::_2);
        }

        bool DownloadOne(const std::string& uri, std::FILE* file);
        bool GetPack(const std::string& script, const std::string& game,
                const std::string& version);

private:

        void CalcStatistics();
        bool MakeRequest(const std::string& uri);
        bool Download(std::FILE* file);
        void CompleteOne();

        bool                    mIsOk = false;
        sf::TcpSocket   mSocket;
        std::string             mSite;
        sf::Clock               mClock;
        size_t                  mCurrentSize = 0;       // size of current downloading file if known
        size_t                  mCurrentTotal = 0;      // amount of downloaded bytes for current file
        float                   mCurrentPercents = 0.0f;
        size_t                  mAllSize = 0;           // total size for all files
        size_t                  mTotal = 0;                     // total downloaded
        float                   mAllPercents = 0.0f;
        unsigned int    mSpeed = 0;
        float                   mLastTime = 0;

        DownloadUpdateCallback mUpdateCallback;
};



#endif
 

#include "pch.h"
#include "net/httpdownload.h"



#define UPDATE_INTERVAL 1.0f



static const std::string CONTENT_LENGTH = "Content-Length";


//////////////////////////////////////////////////////////////////////////
/// HttpAnswer



const std::string& HttpAnswer::GetValue(const std::string& name) const
{
        assert(HasField(name));
        return mFields.find(name)->second;
}



bool HttpAnswer::GetAsUnsigned(const std::string& name, size_t& res) const
{
        const std::string& val = GetValue(name);
        char* end;
        auto value = strtoul(val.c_str(), &end, 10);

        if (errno != ERANGE)
        {
                res = value;
                return true;
        }

        return false;
}



bool HttpAnswer::GetLine(sf::TcpSocket& socket, std::string& line)
{
        line.clear();
        char ch;
        size_t received = 0;

        while (socket.receive(&ch, 1, received) == sf::Socket::Done)
        {
                if (received == 0)
                {
                        return false;
                }


                if (ch != '\r' && ch != '\n')
                {
                        line += ch;
                }

                if (ch == '\n')
                {
                        break;
                }
        }

        return true;
}



int HttpAnswer::ParseFirstLine(const std::string& line)
{
        // HTTP/1.1 200 OK
        std::string beg = line.substr(0, 5);

        if (beg != "HTTP/")
        {
                return -1;
        }

        mVersion = line.substr(5, 3);
        std::string code = line.substr(9);
        char* end;
        auto val = strtoul(code.c_str(), &end, 10);

        if (errno != ERANGE)
        {
                return val;
        }

        return -1;
}



int HttpAnswer::ReceiveAnswer(sf::TcpSocket& socket)
{
        std::string line;
        int res = 0;
        bool first = true;

        do
        {
                if (!GetLine(socket, line))
                {
                        return -1;
                }

                if (line.empty())
                {
                        break;
                }

                if (first)
                {
                        first = false;
                        res = ParseFirstLine(line);

                        if (res != HTTP_OK)
                        {
                                return res;
                        }
                }

                size_t pos = line.find_first_of(':');

                if (pos != std::string::npos)
                {
                        std::string field = line.substr(0, pos);
                        assert(!HasField(field));
                        pos += 2;

                        if (pos < line.length())
                        {
                                mFields[field] = line.substr(pos);
                        }
                }
        } while (!line.empty());

        return res;
}


//////////////////////////////////////////////////////////////////////////
/// HttpDownload


HttpDownload::~HttpDownload()
{
        mSocket.disconnect();
}



bool HttpDownload::Connect(const std::string& site)
{
        assert(!mIsOk);

        mSite = site;
        sf::Socket::Status status = mSocket.connect(mSite.c_str(), 80);
        mIsOk =  status == sf::Socket::Done;
        return mIsOk;
}



inline void HttpDownload::CalcStatistics()
{
        auto elapsed = mClock.getElapsedTime();
        auto seconds = elapsed.asSeconds();
        mSpeed = seconds > 0 ? (unsigned) ((float) mCurrentTotal / seconds) : 0;
        mCurrentPercents = 0;

        if (mCurrentSize != 0)
        {
                mCurrentPercents = (float) mCurrentTotal / (float) mCurrentSize;
        }

        mAllPercents = 0;

        if (mAllSize != 0)
        {
                mAllPercents = (float) mTotal / (float) mAllSize;
        }

        if (seconds - mLastTime > UPDATE_INTERVAL)
        {
                mLastTime = seconds;
                UpdateInfo info;

                if (mCurrentSize > 0)
                {
                        info.mCurrentComplete = mCurrentPercents;
                }
                info.mSpeed = mSpeed;

                if (mUpdateCallback)
                {
                        mUpdateCallback("", info);
                }
        }
}



void HttpDownload::CompleteOne()
{
        UpdateInfo info;
        info.mCurrentComplete = 1.0f;
        info.mSpeed = 0;

        if (mUpdateCallback)
        {
                mUpdateCallback("", info);
        }
}



bool HttpDownload::DownloadOne(const std::string& uri, std::FILE* file)
{
        if (!MakeRequest(uri))
        {
                return false;
        }

        HttpAnswer answer;

        if (answer.ReceiveAnswer(mSocket) != HTTP_OK)
        {
                return false;
        }

        if (answer.HasField(CONTENT_LENGTH))
        {
                if (!answer.GetAsUnsigned(CONTENT_LENGTH, mCurrentSize))
                {
                        // warning
                }
        }

        return Download(file);
}



bool HttpDownload::MakeRequest(const std::string& uri)
{
        char* templ =
                "GET %s HTTP/1.1\r\n"
                "Host: %s\r\n"
                "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0\r\n"
                "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
                "Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3\r\n"
                "Accept-Encoding: chunked\r\n"
                "Vary: Accept-Encoding\r\n"
                "Content-Type: application/octet-stream\r\n"
                "Connection: keep-alive\r\n\r\n";

        const size_t bufsize = 512;
        char dest[bufsize];
#if defined(WINDOWS)
        sprintf_s(dest, bufsize, templ, uri.c_str(), mSite.c_str());
#else
        sprintf(dest, templ, uri.c_str(), mSite.c_str());
#endif
        std::string req(dest);
        return mSocket.send(req.c_str(), req.length()) == sf::Socket::Done;
}



bool HttpDownload::Download(std::FILE* file)
{
        assert(file);
        mCurrentTotal = 0;
        mLastTime = 0.0f;
        const size_t size = 4096;
        char* buf[size];
        size_t received = 0;
        sf::Socket::Status status;
        mClock.restart();

        while ((status = mSocket.receive(buf, size, received)) == sf::Socket::Done &&
                received > 0)
        {
                mCurrentTotal += received;
                mTotal += received;
                std::fwrite(buf, 1, received, file);
                CalcStatistics();
        }

        CompleteOne();
        return mCurrentSize == 0 ? true : mCurrentTotal == mCurrentSize;
}



bool HttpDownload::GetPack(const std::string& script, const std::string& game,
        const std::string& version)
{
        char* templ = "%s?gameId=%s&version=%s";
        const size_t bufsize = 512;
        char dest[bufsize];
#if defined(WINDOWS)
        sprintf_s(dest, bufsize, templ,
                script.c_str(), game.c_str(), version.c_str());
#else
        sprintf(dest, templ,
                script.c_str(), game.c_str(), version.c_str());
#endif
        std::string req(dest);

        if (!MakeRequest(req))
        {
                return false;
        }

        HttpAnswer answer;

        if (answer.ReceiveAnswer(mSocket) != HTTP_OK)
        {
                return false;
        }

        size_t len = 0;

        if (!answer.HasField(CONTENT_LENGTH) ||
                !answer.GetAsUnsigned(CONTENT_LENGTH, len))
        {
                return false;
        }

        char* buf = (char*) malloc(len);
        size_t received = 0;
        mSocket.receive(buf, len, received);

        if (received != len)
        {
                return false;
        }

        /// TODO: parse JSON array

        std::string s(buf, len);
        printf("%s\n", s.c_str());
        free(buf);

        return true;
}
 

How i plan to use it:

void UpdateStats(const std::string& fname, const HttpDownload::UpdateInfo& info)
{
        printf("Loaded %.3d%% av. speed: %dKb/s      \r",
                (int) (info.mCurrentComplete * 100), info.mSpeed / 1024);
}



int _tmain(int argc, _TCHAR* argv[])
{
        std::srand((unsigned)time(0));
        HttpDownload http;
       
        if (http.Connect("www.familyxoft.ru"))
        {
                std::FILE* file = std::fopen("test.pak", "wb");
               
                if (!file)
                {
                        printf("Can't open file for writing\n");
                        return 1;
                }

                http.SetUpdateCallback(&UpdateStats);
                //http.GetPack("/path/getfiles.php", "1", "1"); // downloading several files
                http.DownloadOne("/path/to/file/res.pak", file);
                std::fclose(file);
        }
    return 0;
}
 
« Last Edit: April 27, 2014, 08:18:05 am by ChronicRat »