Announcement

Collapse
No announcement yet.

Dumb Question On Control Creation

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

  • Dumb Question On Control Creation

    If I create a series of DDT controls in a THREAD, does the controls belong to the thread or the main program?

    I'd think they belonged to the main program, except they seem to be destroyed when the thread exits, ...

    Specifically: Am using a thread to populate a bunch of GRAPHIC controls with images from a folder, the main program, when in debug mode, generates:
    Break on error 5:
    Illegal function call
    on encountering each:
    GRAPHIC ATTACH CB.HNDL, imgHndls(ThisImage).imgGraphic
    Where imgHndls() is a UDT structure and .imgGraphic is the ID assigned to the control.
    Furcadia, an interesting online MMORPG in which you can create and program your own content.

  • #2
    There are several possibilities for the error(s), perhaps you could post code that you think should compile so we can narrow down the problem.
    The controls belong to the Parent window, thus the main process, I would think.
    Is the UDT a global array?
    Rod
    "To every unsung hero in the universe
    To those who roam the skies and those who roam the earth
    To all good men of reason may they never thirst " - from "Heaven Help the Devil" by G. Lightfoot

    Comment


    • #3
      Yes, the UDT is global. And the single DWORD being passed to the THREAD is CB.HNDL.
      Pretty sure, this is the test I used:
      CONTROL HANDLE CB.HNDL, imgHndls(ThisImage).imgGraphic TO hGraphic
      GRAPHIC ATTACH CB.HNDL, imgHndls(ThisImage).imgGraphic

      And hGraphic is Zero.

      Attached ZIP is the program plus needed includes.

      Compiling with 9.04.0122
      Attached Files
      Furcadia, an interesting online MMORPG in which you can create and program your own content.

      Comment


      • #4
        If I create a series of DDT controls in a THREAD, does the controls belong to the thread or the main program?
        I'd think they belonged to the main program, except they seem to be destroyed when the thread exits, ...
        Controls belong to the dialog.
        DIALOG NEW hParent& argument is normally passed %HWND_DESKTOP which is 0.

        Somebody else will have to explain how hParent& would be set
        and the styles needed to make dialogs children of other dialogs.
        If there are 3 dialogs and you want dialog2 and dialog3 to be children
        of dialog1, what would be the value of hParent in each dialog?

        I couldn't get the program to compile with or without Jose Roca includes.
        I see the dialog is created in main dialog and thread function below performs actions upon it.

        THREAD FUNCTION ScanFolder(BYVAL hDlg AS DWORD) AS LONG
        Last edited by Mike Doty; 22 Jan 2016, 01:39 AM.

        Comment


        • #5
          There seems to be an issue in this block of code
          Code:
          Recurse = 0
                                  TREEVIEW SET EXPANDED  CB.HNDL, %IDTV_FOLDER_LIST, pItem, 1
                                  DO
                                      IF (Recurse > UBOUND(Folders())) THEN EXIT DO
                                      ThisPath = PARSE$(Folders(Recurse), $TAB, 1)
                                      pItem = VAL(PARSE$(Folders(Recurse), $TAB, 2))
                                      ThisFolder = DIR$(ThisPath &"\*", %SUBDIR)
                                      DO
                                          IF (LEN(ThisFolder) = 0) THEN EXIT DO
                                          IF (ISFOLDER(ThisPath &"\"& ThisFolder)) THEN
                                              TREEVIEW INSERT ITEM  CB.HNDL, %IDTV_FOLDER_LIST, pItem, %TVI_SORT, 0, 0, ThisFolder TO cItem
                                              REDIM PRESERVE Folders(FolderIndex)
                                              Folders(FolderIndex) = ThisPath &"\"& ThisFolder & $TAB & FORMAT$(cItem)
                                              INCR FolderIndex
                                          END IF
                                          TREEVIEW SET EXPANDED  CB.HNDL, %IDTV_FOLDER_LIST, pItem, 1
                                          ThisFolder = DIR$(NEXT)
                                      LOOP
                                      INCR Recurse
                                  LOOP
          And from the docs on DIR$:
          However, you should be aware that specifying a new mask$ parameter always starts an entirely new DIR$ search loop.
          I think that first DIR$ statement should be before the loop, not in it. I can't get the program to pass this point. This may be related to the issue in the debugger.

          On an unrelated matter, this IF/THEN|ELSE|END IF block doesn't compute, well not in my head, but it compiles.
          Code:
          IF (0 = 0) THEN
                                          @TBTP.@lpszText = "Exit The Program"
                                      ELSE
                                          @TBTP.@lpszText = "Close This Window"
                                      END IF
          0 is always going to be equal to 0 so the ELSE clause is wasted typing.
          Rod
          "To every unsung hero in the universe
          To those who roam the skies and those who roam the earth
          To all good men of reason may they never thirst " - from "Heaven Help the Devil" by G. Lightfoot

          Comment


          • #6
            Further study of that block of code I posted, I don't see anything wrong with the way you have the DIR$ set up. It must be some other aspect that is causing it to hang at that point though.
            Rod
            "To every unsung hero in the universe
            To those who roam the skies and those who roam the earth
            To all good men of reason may they never thirst " - from "Heaven Help the Devil" by G. Lightfoot

            Comment


            • #7
              Hi Glen,

              Any thread in your process can create controls. The thread that creates the control owns the control and the system provides a message queue to maintain it. That message queue is destroyed when the thread ends.

              Normally you would have a GUI Thread to maintain your UI elements with Worker Threads beavering away in the background, sending messages to the UI thread if new controls / UI elements are required.

              Here's some sample code to investigate how that works.
              Code:
              #COMPILE EXE
              #DIM ALL
              #INCLUDE "WIN32API.INC"
              '------------------
               
              THREAD FUNCTION NewThread(BYVAL hWin AS DWORD) AS LONG
                CONTROL ADD BUTTON, hWin, 222,  "Thread Btn", 140, 150,  50, 15
                DIALOG SEND hWin, %WM_APP + 101, 223, 0
                  MessageBox 0, "In Thread","Holding", %MB_SYSTEMMODAL ' Without loop NewThread dies when msgbox released
                  GRAPHIC ATTACH hWin, 111
                  GRAPHIC CLEAR %BLUE
               '  DO                                ' Remove comments to keep NewThread alive
               '    DIALOG DOEVENTS
               '  LOOP WHILE ISWIN(hWin)
              END FUNCTION
              '------------------/NewThread
               
              CALLBACK FUNCTION DlgProc()
                SELECT CASE AS LONG CBMSG
                  CASE %WM_INITDIALOG
                    DIALOG POST CBHNDL, %WM_USER + 1000, 0, 0
               
                  CASE %WM_USER + 1000
               
                   CASE %WM_APP + 101
                      CONTROL ADD GRAPHIC, CB.HNDL, 111, "", 10, 45, 35, 35, %WS_BORDER
                      CONTROL ADD BUTTON,  CB.HNDL, CB.WPARAM, "GUI Btn", 140, 120,  50, 15   '(wparam = 223)
               
                  CASE %WM_COMMAND
                    SELECT CASE AS LONG CBCTL
                      CASE %IDOK
                        IF CBCTLMSG = %BN_CLICKED THEN
                         LOCAL hThread, lRes AS LONG
                          THREAD CREATE NewThread(CBHNDL) TO hThread
                          THREAD CLOSE hThread TO lRes : hThread = 0
                        END IF
                      CASE 222
                        IF CBCTLMSG = %BN_CLICKED THEN
                         GRAPHIC ATTACH CB.HNDL, 111
                         GRAPHIC CLEAR %RED
                        END IF
                      CASE 223
                        IF CBCTLMSG = %BN_CLICKED THEN
                         GRAPHIC ATTACH CB.HNDL, 111
                         GRAPHIC CLEAR %GREEN
                        END IF
                      CASE %IDCANCEL
                        IF CBCTLMSG = %BN_CLICKED THEN
                          DIALOG END CBHNDL
                        END IF
                    END SELECT
               
                END SELECT
              END FUNCTION
              '------------------/DlgProc
               
              FUNCTION PBMAIN()
               LOCAL hDlg AS DWORD
                DIALOG NEW 0, "Test", 265, 165, 200, 180, %WS_CAPTION OR %WS_SYSMENU TO hDlg
                  CONTROL ADD LABEL,  hDlg, 201,    "Test Dialog",  5, 5, 120, 15
                  CONTROL ADD BUTTON, hDlg, %IDOK,  "Test",       140, 5,  50, 15
               
                DIALOG SHOW MODAL hDlg, CALL DlgProc
              END FUNCTION
              '------------------/PbMain
              Rgds, Dave

              Comment


              • #8
                Original question...
                f I create a series of DDT controls in a THREAD, does the controls belong to the thread or the main program?
                ...and the response ==>
                Any thread in your process can create controls. The thread that creates the control owns the control and the system provides a message queue to maintain it. That message queue is destroyed when the thread ends.
                Um, kind of; and not without restriction.

                A child window (control) can only be created within the thread context of the owning window and its window procedure will always execute in that thread context.

                That is, this is not allowed:
                Code:
                 
                  Hwnd =  CreateWindowEx  (any styles except WS_CHILD)) 
                
                  THREAD CREATE   AddControls (hWnd) 
                 ....
                
                THREAD FUNCTION AddControls (BYVAL hWnd AS LONG ) AS LONG 
                
                    hCtrl = createWindowEx ( owner = hWnd; style includes WS_CHILD)
                I "imagine" the same rules apply to using DIALOG NEW and CONTROL ADD as apply to CreateWindowEx but I am not a DDT guy.

                But this is allowed

                Code:
                  DIALOG NEW / CreateWindowEx  ..
                     CONTROL ADD  / CreateWindowEx 
                
                  THREAD CREATE MakeMyScreen (someparam) 
                
                 ...
                
                THREAD FUNCTION MakeMyScreen (someparam) 
                
                
                     DIALOG NEW/CreateWindowEx
                        CONTROL ADD / CreateWindowEx  (Owner and WS_CHILD specified)
                That is, the process is running two threads of execution, each thread being a GUI thread, and the two windows and their owned controls are owned by the thread in which context it was created.

                DEMO (BION!)
                Progress Bar Dialog for PB/CC programs October 24, 2002
                (Ignore the "PB/CC" in the thread title. This will work in a GUI program exactly the same way).

                MCM
                Last edited by Michael Mattias; 22 Jan 2016, 09:49 AM.

                Comment


                • #9
                  Three questions

                  Thank you for the code, Dave Biggs!
                  Any thread in your process can create controls. The thread that creates the control owns the control and the system provides a message queue to maintain it. That message queue is destroyed when the thread ends.

                  Normally you would have a GUI Thread to maintain your UI elements with Worker Threads beavering away in the background, sending messages to the UI thread if new controls / UI elements are required.
                  CONTROL ADD is used multiple times.
                  The buttons are created on top of each other so made a modification.
                  1) Does CONTROL ADD ever fail?
                  2) Can/should multiple buttons have the same ID and be stacked on top of each other?

                  Code was very useful in pointing out a problem in many programs.
                  If other threads have a MSGBOX it may never get time.
                  Added DOEVENTS to the code below. Please do not discuss WaitForSingle/Multiple objects.
                  Code:
                   FUNCTION PBMAIN AS LONG
                   ...
                   ...
                   
                     DO
                       SLEEP 500
                       DIALOG DOEVENTS  
                     LOOP UNTIL threadcount = 1
                   END FUNCTION
                  Made a few modifications to not stack up the buttons and main dialog modeless.
                  3) Is there a way to tell which thread issued the button click?

                  Code:
                  #COMPILE EXE
                  #DIM ALL
                  #INCLUDE "WIN32API.INC"
                  '------------------
                  GLOBAL gNewThreadCounter AS LONG  'added 1
                   
                  THREAD FUNCTION NewThread(BYVAL hWin AS DWORD) AS LONG  'modified Dave Biggs code
                    LOCAL counter AS LONG
                    counter = gnewThreadCounter
                    CONTROL ADD BUTTON, hWin, 222,  "From Thread"+STR$(gNewThreadCounter), 340-(gNewThreadCounter*50), 150,  50, 15 'added 2
                     INCR gNewThreadCounter 'added 3
                    DIALOG SEND hWin, %WM_APP + 101, 223, 0
                    'MessageBox 0, "In Thread","Holding", %MB_SYSTEMMODAL ' Without loop NewThread dies when msgbox released
                    GRAPHIC ATTACH hWin, 111
                    GRAPHIC CLEAR %BLUE
                    DIALOG SET TEXT hWin,"New thread" + STR$(counter)
                    DO                                ' Remove comments to keep NewThread alive
                       DIALOG DOEVENTS
                    LOOP WHILE ISWIN(hWin)
                  END FUNCTION
                   '------------------/NewThread
                  CALLBACK FUNCTION DlgProc()
                    SELECT CASE AS LONG CBMSG
                      CASE %WM_INITDIALOG
                        DIALOG POST CBHNDL, %WM_USER + 1000, 0, 0
                       CASE %WM_USER + 1000
                       CASE %WM_APP + 101
                         CONTROL ADD GRAPHIC, CB.HNDL, 111, "", 10, 45, 35, 35, %WS_BORDER
                         'CONTROL ADD BUTTON,  CB.HNDL, CB.WPARAM, "From GUI", 340, 120,  50, 15   '(wparam = 223) CB.WPARAM
                         CONTROL ADD BUTTON,  CB.HNDL, CB.WPARAM, "From GUI"+STR$(gNewThreadCounter-1), 340-((gNewThreadCounter-1)*50), 120,  50, 15   '(wparam = 223)
                       CASE %WM_COMMAND
                        SELECT CASE AS LONG CBCTL
                          CASE %IDOK
                            IF CBCTLMSG = %BN_CLICKED THEN
                             LOCAL hThread, lRes AS LONG
                              THREAD CREATE NewThread(CBHNDL) TO hThread
                              THREAD CLOSE hThread TO lRes : hThread = 0
                            END IF
                          CASE 222  'thread bin
                            IF CBCTLMSG = %BN_CLICKED THEN
                             GRAPHIC ATTACH CB.HNDL, 111
                             GRAPHIC CLEAR %RED
                            END IF
                          CASE 223  'gui bin
                            IF CBCTLMSG = %BN_CLICKED THEN
                             GRAPHIC ATTACH CB.HNDL, 111
                             GRAPHIC CLEAR %GREEN
                            END IF
                          CASE %IDCANCEL
                            IF CBCTLMSG = %BN_CLICKED THEN
                              DIALOG END CBHNDL
                            END IF
                        END SELECT
                     END SELECT
                  END FUNCTION
                  '------------------/DlgProc
                   FUNCTION PBMAIN()
                   LOCAL hDlg AS DWORD
                    DIALOG NEW 0, "Test", 265, 165, 400, 180, %WS_CAPTION OR %WS_SYSMENU TO hDlg  'added 200 made 400
                    CONTROL ADD LABEL,  hDlg, 201,    "Test Label",  5, 5, 120, 15
                    CONTROL ADD BUTTON, hDlg, %IDOK,  "Create thread",       340, 5,  50, 15               'added 140 made 340
                    DIALOG SHOW MODELESS hDlg,CALL DlgProc 'added changed to modeless
                    DO
                      DIALOG DOEVENTS
                    LOOP WHILE ISWIN(hDlg) AND gNewThreadCounter < 8  'only allow 8 more threads
                    ? "Done, gNewThreadCounter =" + STR$(gNewThreadCounter)
                  END FUNCTION
                  '------------------/PbMain
                  Last edited by Mike Doty; 22 Jan 2016, 12:25 PM. Reason: Tidy up code and add more comments

                  Comment


                  • #10
                    1) Does CONTROL ADD ever fail?
                    It should fail if you execute it in a thread context other than that in which the owner (param #1 to control add) window (dialog) was created. The returned handle ("hCtrl") should be zero. I don't know if ERR will be set or not.

                    It can also fail if you specify an invalid style, or an incompatible combination of styles. These will be control-specific.

                    2) Can/should multiple buttons have the same ID and be stacked on top of each other?
                    Can: Yes.

                    Should: Most definitely No, at least for the "same ID" part. If controls on a screen do not have a unique identifier, you can never send them a message, which is what you need to do to make the control do anything. You will also never be able to tell which button is sending a notification to you. Both are "not good."

                    "Stacking" is application specific. If that's what you want the screen to look like and the way you want it to interact with the user, go ahead. There are no restrictions against two or more controls occupying the same real estate.

                    MCM

                    Comment


                    • #11
                      Hi Mike,

                      Good modifications. - Probably my test would have been tidier if the Create thread' button had been disabled after use
                      In response to your questions..
                      1) Does CONTROL ADD ever fail?
                      Control Add.. is a DDT source code Statement - there is no return value. If the statement is malformed the compiler will complain.
                      The pogrammer is of course free to create logic bombs or useful stuff with the resultant creation
                      (BTW There is no error code listed re: "Not allowed from this thread" or similar that I could see in PB's list).

                      2) Can/should multiple buttons have the same ID and be stacked on top of each other?
                      It's up to the programmer to assign appropriate ID numbers to the controls - without unique IDs it is more difficult to interact with the controls at run time.
                      Stacking.. possibly useful in a UI where the program controls the active / visible states of the controls?

                      3) Is there a way to tell which thread issued the button click?
                      Button Click notifications are generally sent to the program's Callback Functions by the system in response to user interactions.
                      The Callback Function used is as specified by the CALL DLGPROC clause of the DIALOG SHOW statement of the parent, or by the CALL CTLPROC clause of the CONTROL ADD statement.
                      There may be scope to have a different CTLPROC for each control based upon the thread which created it?
                      Just for fun, further experimentation might be interesting..
                      Last edited by Dave Biggs; 22 Jan 2016, 07:26 PM.
                      Rgds, Dave

                      Comment


                      • #12
                        Whelps, that answers that, now on to plan B, as soon as I can think of one that is, ...

                        Probably go with the nasty of storing the graphic image handles generated by the GDI+ into the UDT and generating the graphic controls on the fly when needed for display. Would dispense with the controls if it wasn't for that I want click notifications and don't feel like writing a hittest for clicking on the client screen and guessing what image was clicked on.
                        Furcadia, an interesting online MMORPG in which you can create and program your own content.

                        Comment


                        • #13
                          Originally posted by Rodney Hicks View Post
                          On an unrelated matter, this IF/THEN|ELSE|END IF block doesn't compute, well not in my head, but it compiles.
                          Code:
                          IF (0 = 0) THEN
                              @TBTP.@lpszText = "Exit The Program"
                          ELSE
                              @TBTP.@lpszText = "Close This Window"
                          END IF
                          0 is always going to be equal to 0 so the ELSE clause is wasted typing.
                          Ha, forgot to remove that from the sample, the test (0=0) is a placeholder for a future test to determine if the program had created a child window of itself using the same callback. Hence the different messages involved.
                          Furcadia, an interesting online MMORPG in which you can create and program your own content.

                          Comment


                          • #14
                            3) Is there a way to tell which thread issued the button click?
                            A thread of execution does not issue a button click.

                            A notification message from a control is always provided in the context of the thread owning the window/control.

                            Probably go with the nasty of storing the graphic image handles generated by the GDI+ into the UDT and generating the graphic controls on the fly when needed for display. Would dispense with the controls if it wasn't for that I want click notifications and don't feel like writing a hittest for clicking on the client screen and guessing what image was clicked on.
                            Use a click-detecting control as a 'container' for your image and now you will get "regular" notifications - without writing a 'hit test.'

                            E.g. if you use a "static" control with style SS_NOTIFY, you will get WM_COMMAND/STN_CLICK and WM_COMMAND/STN_DBLCLK notifications regardless of what is currently drawn on that control.

                            I would think you should be able to use a static control with either SS_ICON or SS_BITMAP in addition to SS_NOTIFY to get exactly what you want. If you can't get a BITMAP from your image, you can use SS_OWNERDRAW and paint your image on the WM_DRAWITEM notification.. and still get your 'click' notifications.

                            [ADDED]
                            Um, duh, "button" controls offer bitmap, icon and ownerdraw options, too.

                            Did you maybe think "GRAPHIC" control and then stop considering other possible HOWs to accomplish your WHAT?

                            MCM
                            Last edited by Michael Mattias; 25 Jan 2016, 09:02 AM.

                            Comment


                            • #15
                              Hi Colin,

                              As a 'Plan B' you could maybe make a call to the main thread (that owns the GUI) to create the graphic controls while the worker thread does the rest?

                              eg something like this in the thumbsorted_v2 code that you posted..
                              Code:
                                        ' Add Global rect to hold Graphic control dimensions x&, y&, wide&, high& 
                                         Global grc AS RECT
                               
                                        ' Replace this Statement in Thread Function ScanFolder()
                                        ' CONTROL ADD GRAPHIC, hDlg, imgHndls(ThisImage).imgGraphic, "", 0, 0, xAspect, yAspect
                                        ' with
                                          grc.nLeft = -ghThumbSize : grc.nTop = -ghThumbSize
                                          grc.nRight = xAspect : grc.nBottom = yAspect
                                          SendMessage hDlg, %WM_APP + 101, ThisImage, 0
                              
                                         'and ADD this Case in ShowDifferentialImageGeneratorProc()
                              
                                       CASE %WM_APP + 101
                                        CONTROL ADD GRAPHIC, CbHndl, imgHndls(CbwParam).imgGraphic, "", grc.nLeft, grc.nTop, grc.nRight, grc.nBottom, _
                                                %WS_CHILD OR %SS_NOTIFY
                              With that, the Graphic controls persist beyond the end of the worker thread
                              Rgds, Dave

                              Comment


                              • #16
                                As a 'Plan B' you could maybe make a call to the main thread (that owns the GUI) to create the graphic controls while the worker thread does the rest?
                                You do not "call" a thread. A thread executes its thread function, period. The thread object ceases to exist when it reaches the end of its thread function and all handles to the thread have been closed.

                                The thread function "may be " set up to wait for notifications (eg via a synchonization object + Wait function). In this case it can "seem like" you are "calling a thread" but you are not.

                                A "typical" design when a "worker" thread of execution is used is to have the worker thread function send a message to the GUI window telling it to "do something" because "something" happened during the the execution of the thread function.

                                eq you might have a GUI thread in which you want to display a list of files found. You "might" design this to do something like..
                                Code:
                                  FUNCTION  Main_Window_Procedure 
                                
                                  DO 
                                
                                          IF User-has-selected-a-file-spec 
                                              THREAD CREATE GetDirList ( hwnd, spec)  << NOT LEGAL I KNOW 
                                         END IF 
                                
                                      IF Notrification-of-file-matching-spec received 
                                         Add file to list control containing file list 
                                      END IF 
                                      .... 
                                LOOP
                                
                                THREAD FUNCTION GetDirList (hWnd_to_notify, file_spec$) 
                                
                                
                                       S = DIR$ (File_spec$) 
                                       WHILE LEN (S) 
                                            Postmessage Hwnd_to_notify, %USER_MESSAGE, FileName, %NULL
                                            S = DIR$ (NEXT)
                                      WEND 
                                      DIR$ CLOSE
                                
                                END FUNCTION
                                MCM

                                Comment


                                • #17
                                  You do not "call" a thread.
                                  "make a call to the main thread" > "call upon the main thread" ??

                                  Forgive my FTD but anyway semantic analysis is moot when actual code is provided.
                                  Rgds, Dave

                                  Comment


                                  • #18
                                    > ... semantic analysis is moot when actual code is provided.

                                    I truly believe that there are certain words which must in certain contexts (e.g, "Windows programming fora" ) be used very carefully.

                                    "Call" is one of those words.

                                    You might understand that what is said may not be what was meant or what some posted code actually shows; but that's only true because you are an experienced Windows' programmer.

                                    But we still have "newbies" here and IMNSHO when one of them sees "Call a thread" they just might not know - yet - that there is no such thing.


                                    MCM

                                    Comment


                                    • #19
                                      From my perspective, I think that's just an attempt to rationalize annoying nit picking

                                      If you really did care about misdirecting "newbies" you would be more careful what you post between [code] tags or at least explain why it's "illegal"

                                      OMMV
                                      Rgds, Dave

                                      Comment


                                      • #20
                                        Such nitpicking, ...

                                        Dave, I considered "calling" the main thread, (e.g. DIALOG POST), however, the issue would be that I'd have to keep careful track of the GDI+ objects in circulation, and dispose of them properly when finished. Figured I'd be saving myself some clean up by using DDT Graphic objects.

                                        Michael, the above was a "plan B", but on further consideration, not as nice userwise as I think it ought to be.

                                        Using statics or buttons, that's a nice idea.

                                        Need to have mouseover detection though, and the idea of wrapping a few hundred controls with a common subclass is NOT very good programming practice.

                                        So my second idea (using graphic controls) is not quite desirable.

                                        Looking back at my original idea of writing a "Canvas Object" that manages the display of the thumbnails is more like the direction I want to lean towards.
                                        Furcadia, an interesting online MMORPG in which you can create and program your own content.

                                        Comment

                                        Working...
                                        X