Author Topic: Upload and download level files from GLBasic to server  (Read 2136 times)

Offline BdR

  • Dr. Type
  • ****
  • Posts: 303
    • View Profile
    • BdR Games
I'm working on a puzzle game for the iphone using GLBasic and it will have a level editor so players can make their to own puzzles. A level is simply a grid with items, similar to Sokoban levels (but my game is not Sokoban ;))

But I also want to be able to send these user created levels to a website or server. Is it possible in GLBasic to send files to a server? What would be the best way to do this. I guess you could use PHP scripts, similar to the php script to send and recieve online-highscores.. But I don't know that much about it, how would you send an entire file using php? :doubt:

On the news page it says that Wumbo's Adventure will have an online level library so I guess it is possible. Will the next version of GLBasic have added features or commands to do these kind of things?

Offline Moru

  • Administrator
  • Prof. Inline
  • *******
  • Posts: 1775
    • View Profile
    • Homepage
I have some examples on my homepage on how to send a file to an apache webserver with PHP and MySQL. Fully running examples with source for both client and server.

Offline Kitty Hello

  • code monkey
  • Administrator
  • Prof. Inline
  • *******
  • Posts: 10714
  • here on my island the sea says 'hello'
    • View Profile
    • http://www.glbasic.com
OK, here's how I do it:

Code: GLBasic [Select]
FUNCTION UploadLevel%: nLevel%
        LOCAL out$, ok%
LOCAL file$ = file to upload
[...]
        ok% = WumboStore("action=host&review", out$, file$)

ENDFUNCTION



FUNCTION GetAllLevels:
        GETFILE file$+"/updateinfo", 0, my_version%

        DEBUG "Local version: "+my_version%+"\n"

// get level list and update version
        LOCAL levels$
        IF WumboStore("action=list"+review$, levels$)=FALSE
                RETURN FALSE
        ENDIF

        LOCAL remoteversion%=INTEGER(levels$)
        IF my_version% >= remoteversion%
                // ConsolePrt(lang$("Levels are up to date", "Level sind auf aktuellem Stand"))
                RETURN TRUE
        ENDIF

        // finally download the levels
        LOCAL lv$[]
        SPLITSTR(levels$, lv$[], "\n")
        DIMDEL lv$[], 0
        LOCAL i%=0
        FOREACH lev$ IN lv$[]
                INC i, 1
                LOCAL prtlev$
                prtlev$ = lev$
                IF LEN(prtlev$)>30 THEN prtlev$="..." + MID$(prtlev$, LEN(prtlev$)-30)
                IF WumboStore("action=send&level="+lev$+review$, levels$)
                        LOCAL file% = GENFILE()
                        IF OPENFILE(file%, file$+"/"+lev$, 0)
                                WRITESTR file%, levels$
                                CLOSEFILE file%
                        ENDIF
                ELSE
                        RETURN FALSE
                ENDIF
        NEXT

        // save the updateinfo version to the local drive
        IF OPENFILE(0, GetEditLevelDir$()+"/updateinfo", 0)
                WRITELINE 0, remoteversion%
                CLOSEFILE 0
        ENDIF
ENDFUNCTION

FUNCTION WumboStore%: get$, BYREF out$, levelfile_upload$=""
        out$="error"

        LOCAL boundary$, postdata$
        // make a boundary string. That's a few "--" and some random characters
        boundary$ = "----------------bOunDarY-------"+INTEGER(GETTIMERALL()*1000.0)+"-"+RND(99999)+"--"
        IF LEN(levelfile_upload$)
                INC postdata$, AddHttpPostFile$("level", levelfile_upload$, boundary$, "text/plain")
        ENDIF

        LOCAL server$="www.glbasic.com" // the server URL
        LOCAL curl$  ="/wumbo/wumbolev.php?" + get$ // the path on the server (start with / )
        LOCAL proxy$ =""  // leave this empty for a direct connection
        LOCAL port%=80                // use port 80 for direct connection
        LOCAL ret$

        out$ = HttpPost$(proxy$, server$, curl$, port%, boundary$, postdata$)

        DEBUG out$ + "\n\n"

        IF INSTR(out$, "$$error")>=0 OR LEN(out$)=0 THEN RETURN FALSE
RETURN TRUE
ENDFUNCTION





// ---------------------------------------------------------------------
// Send a HTTP POST Request and return what the webbrowser would display
// ---------------------------------------------------------------------
FUNCTION HttpPost$: proxy$, server$, curl$, port%, boundary$, postdata$
LOCAL header$ // the HTTP post header

        // Fix proxy urls
        IF LEN(proxy$)
                curl$ = "http://"+server$+curl$
                server$ = proxy$
        ENDIF

        // end post data
        postdata$ = postdata$ + "--"+boundary$+"--"

LOCAL send$
        send$ = "POST "+curl$+" HTTP/1.0\r\n"
        INC send$, "User-Agent: GLBasic\r\n"
        INC send$, "Host:"+server$+"\r\n"
        INC send$, "Accept: */*\r\n"
        INC send$, "Content-Type: multipart/form-data, boundary="+boundary$+"\r\n"
        INC send$, "Content-Length: "+LEN(postdata$) + "\r\n"
        INC send$, "\r\n"
        INC send$, postdata$

LOCAL sock%, file$
        SOCK_INIT()
        sock% = SOCK_TCPCONNECT(server$, port%)
        IF sock%<0
                ConsolePrt(lang$("Connection failed - Wifi active?", "Verbindung fehlgeschlagen. Wifi an?"))
                STDERR "Can&#39;t connect to "+server$+"\n"
                STDERR NETGETLASTERROR$()+"\n"
                RETURN ""
        ENDIF
        IF SOCK_TCPSEND(sock%, send$) > 0
                LOCAL chunk$, rv%
                WHILE TRUE
                        chunk$=""
                        rv% = SOCK_RECV(sock%, chunk$, 1024)
                        SELECT rv%
                                CASE -1
                                        STDERR NETGETLASTERROR$()+"\n"
                                        file$=""
                                        BREAK
                                CASE -2 // non blocking socket not would block
                                        SLEEP 5
                                CASE 0
                                        BREAK // ok, reading is done
                                CASE >0
                                        INC file$, chunk$
                        ENDSELECT
                WEND
                // jippieh! We got a response

                // split HTTP header and actual file information apart
                LOCAL pos%
                pos = INSTR(file$, "\r\n\r\n")
                IF pos>=0
                        header$ = MID$(file$, 0, pos%)
                        file$   = MID$(file$, pos%+4)
                ELSE
                        header$=""
                ENDIF
        ELSE
                STDERR "can&#39;t open socket to "+server$+"\n"
        ENDIF
        SOCK_CLOSE(sock%)

        RETURN file$
ENDFUNCTION




// ---------------------------------------------------------------------
// add a variable to a HTTP POST query
// ---------------------------------------------------------------------
FUNCTION AddHttpPostVar$: varname$, value$, boundary$
LOCAL postdata$
        postdata$ = "--"+boundary$+"\r\nContent-Disposition: form-data; name=\""+varname$+"\"\r\n\r\n"+value$+"\r\n"
        RETURN postdata$
ENDFUNCTION

// ---------------------------------------------------------------------
// upload a png image variable to a HTTP POST query
// ---------------------------------------------------------------------
FUNCTION AddHttpPostFile$: varname$, filepath$, boundary$, content_type$
LOCAL postdata$
LOCAL content$

        IF OPENFILE(1, filepath$, TRUE)
                LOCAL b%
                WHILE ENDOFFILE(1)=FALSE
                        READBYTE 1, b%
                        // chr$(0) is a string of length "1", that contains a &#39;\0&#39; character!
                        content$ = content$ + CHR$(b%)
                WEND
                CLOSEFILE 1

                postdata$ = "--"+boundary$+"\r\n"
                INC postdata$, "Content-Disposition: form-data; name=\""+varname$+"\"; filename=\""+filepath$+"\"\r\n"
                INC postdata$, "Content-Transfer-Encoding: binary\r\n"
                IF LEN(content_type$)
                        INC postdata$, "Content-Type: "+content_type$+"\r\n"
                ENDIF
                INC postdata$, "Content-Type: application/octet-stream\r\n\r\n" + content$ + "\r\n";
        ENDIF

        RETURN postdata$
ENDFUNCTION
 


and the php code (part of)

Code: GLBasic [Select]
<?php
        $DOCUMENT_ROOT = $_SERVER[&#39;DOCUMENT_ROOT&#39;];
        if (substr ($DOCUMENT_ROOT, -1) == &#39;/&#39;) { $DOCUMENT_ROOT = substr ($DOCUMENT_ROOT, 0, -1); }

        // global paths
        $http_root="http://www.glbasic.com/wumbo/"; // with trailing slash
    $doc_root ="$DOCUMENT_ROOT/wumbo/"; // with slash
   
// ?action=host        - http_post:level=file
// ?action=list        - list release levels
// ?action=list&review - list review levels
// ?action=hokay&level=levelname - put to release version
// ?action=send&level=levelname  - send to client [&review] possible
// ?action=send&level=levelname  - send to client

    function list_files($directory)
    {
        $handle1 = @opendir($directory);
        if ($handle1)
        {
            while (false !== ($file = readdir($handle1)))
            {
                if ($file!=&#39;.&#39; && $file!=&#39;..&#39; && $file!=&#39;updateinfo&#39; && !is_dir($name) )
                {
                    echo ( $file . "\n" );
                }
            }
            closedir($handle1);
        }
    }
 
 
    // Set release / inreview directory
    $dir = $doc_root;
    if(isset($_GET[&#39;review&#39;]) )
        $dir .= &#39;inreview&#39;;
    else
        $dir .= &#39;release&#39;;
   
    // SEND level to client
    if($_GET[&#39;action&#39;] == &#39;send&#39;)
    {
        exit( file_get_contents( $dir .&#39;/&#39;. $_GET[&#39;level&#39;] ) );
    }

    // HOST LEVEL - inreview directory
    if($_GET[&#39;action&#39;] == &#39;host&#39;)
    {
        // check file size limit (my biggest level is 31k)
        if($_FILES[&#39;level&#39;][&#39;size&#39;] > 55000)
            exit(&#39;$$error - lev too big&#39;);
   
                $file_lower = strtolower($_FILES[&#39;level&#39;][&#39;name&#39;]);
                $accepted_endings=array(  &#39;.lev&#39;, &#39;.pkg&#39; );
       
                if(in_array(substr($file_lower, -4, 4), $accepted_endings))
                {
                        $tmpfile = $_FILES[&#39;level&#39;][&#39;tmp_name&#39;];
                        if(move_uploaded_file($_FILES[&#39;level&#39;][&#39;tmp_name&#39;], $doc_root . &#39;inreview/&#39; . $_FILES[&#39;level&#39;][&#39;name&#39;]))
                        {
                    exit("ok");
                }
                // not, yet
                // exit("ok");
                        }
                        else
                        {
                            echo("$$error - can&#39;t move uploaded file:\n".$doc_root . &#39;inreview/&#39; . $_FILES[&#39;level&#39;][&#39;name&#39;]."\n");
                        }
                }
                else
                {
                    echo "$$error - format not acceted\n";
                }
    }
   
    // LIST all files
    if($_GET[&#39;action&#39;] == &#39;list&#39;)
    {
        echo (int)file_get_contents( $dir .&#39;/updateinfo&#39; ) . "\n";

        list_files($dir);
        exit(&#39;&#39;); // list ok
    }
   
    // Copy a file to release directory, increase the info number
    if($_GET[&#39;action&#39;] == &#39;hokay&#39;)
    {
        if( rename($doc_root.&#39;inreview/&#39;.$_GET[&#39;level&#39;], $doc_root.&#39;release/&#39;.$_GET[&#39;level&#39;]) )
        {
            // also move the package, if changed - ignore warnings
            $package = substr($_GET[&#39;level&#39;], 0, strlen($_GET[&#39;level&#39;])-4) . &#39;.pkg&#39;;
            rename($doc_root.&#39;inreview/&#39;.$package, $doc_root.&#39;release/&#39;.$package);

            $version = (int)file_get_contents( $doc_root.&#39;release/updateinfo&#39; ) + 1;
           
            echo &#39;release version: &#39;.$version."\n";

            // update theupdate info number in the release version
            settype($version, &#39;string&#39;);
            $file = fopen($$doc_root .&#39;release/updateinfo&#39;, &#39;w&#39;);
            fwrite( $file, $version);
            fclose($file);
            exit(&#39;release - ok&#39;);
        }
        exit(&#39;$$error - unable to release&#39;);
    }  

    exit(&#39;$$error - unknown&#39;);
?>
 

Offline BdR

  • Dr. Type
  • ****
  • Posts: 303
    • View Profile
    • BdR Games
Wow, thanks for the examples, this is really helpful. I really like the idea of just using URLs and then base64 string parameters to send the binary data. For now, I'll finish the core of the game so I can decide on the level-file-layout, then I'll start on the php and server side of things. :good: Thanks again.

Offline BdR

  • Dr. Type
  • ****
  • Posts: 303
    • View Profile
    • BdR Games
Okay, it's been a while but recently I've found the time to continue on my project. I'm working on the server side of things and it is going great! :) I can't believe how easy and straight forward it all is. The php scripting is pretty easy too.

Here's how I want to do it:
I do a http post from GLBasic using the code example above, then the php script on the server handles the file and stores it. Also some sender information is sent, using PLATFORMINFO$("LOCALE") and PLATFORMINFO$("ID") etc. and this is also stored. The GLBasic program can request a list of available files, and then downloads each of these files.

:good: This all works now, but I do have 2 questions..

First of all how do you handle a re-upload of the same level or when a user sends an update of his levelfile? I think if I keep the md5-checksum and the PLATFORMINFO$("ID") to identify the sender, and then check for previously uploaded files using these 2 pieces of information will be enough. But any thoughts on this?

Second, how do you handle files with the same filename? For example, first a user sends a file called "mylevel.dat" and then another user also sends his "mylevel.dat". How do you distribute these two files to all other users? As "mylevel.dat" and "mylevel(1).dat"?

Offline BdR

  • Dr. Type
  • ****
  • Posts: 303
    • View Profile
    • BdR Games
oh, and also.. This is probably a n00b question :doubt: but in the httppost example of Hello Kitty

Code: GLBasic [Select]
   // make a boundary string. That&#39;s a few "--" and some random characters
   boundary$ = "----------------bOunDarY-------"+INTEGER(GETTIMERALL()*1000.0)+"-"+RND(99999)+"--"
 

Why do you generate a random boundary for the HTTP-Post message? Why not just use a standard boundary that is the same everytime you post?

Offline Moru

  • Administrator
  • Prof. Inline
  • *******
  • Posts: 1775
    • View Profile
    • Homepage
To store files with identical names you add the ID number from the database to the filename so you will always have a unique name. In the database you store the original name and the new so you can keep track of which file needs replacing when reuploading a file.

When users download a file they get the new unique name so it won't get replaced on downloading two files that was named the same on upload.

I don't know how safe it is to depend on the PLATFORMINFO$("ID") for replacing files though, you can make a game just to harvest PLATFORMINFO$("ID") from users and then you get a complete list of passwords into your database... I would create a password on the server and send back to the user on first upload, save it on disk and when reuploading using that password automaticly.

Good luck with your project, looking forward to play it!

Offline Kitty Hello

  • code monkey
  • Administrator
  • Prof. Inline
  • *******
  • Posts: 10714
  • here on my island the sea says 'hello'
    • View Profile
    • http://www.glbasic.com
I don't know about the boundary random numbers. All samples do it that way.