====== GUIScript Introduction ======

This page is intended as a brief overview for one of two scripting systems in GemRB - GUIScript. You should read it if you want to hack on GemRB's GUI or related logic.


===== Introduction - GUIScript and GameScript =====

As said above, there are two scripting systems within GemRB - GUIScript and GameScript. What's the difference between them?

The first one, GUIScript, concerns mostly with game GUI, i.e. user interaction, opening and closing windows, pushing of character sheet buttons, inventory, etc. This functionality has been hardwired in the original games and so we need to implement it anew. GUIScript scripts have to be tailored for each game type, because each game uses more or less different GUI, string refs, and so on. We program these scripts in Python.

The second one, GameScript, governs almost all AI and "AI" in game, effects of opening a trapped chest, annoying an important NPC or putting together pieces of a puzzle. These scripts are of course totally story (and hence game) dependent, fortunately they are stored in game's data files. They are written in special language and usually compiled in *.bcs files. Some of them are uncompiled (*.bs files) or even stored in dialog.tlk.

This page concerns itself with the first type of scripts, GUIScripts.


===== Script execution =====

GUIScripts are stored in gemrb/GUIScripts/<gametype>/. First script to be run is always named Start.py. Main window with messages, map display, character portraits, action buttons etc. is named MessageWindow.py. Other scripts are usually named after *.CHU resource file they mostly use.

  Example:
    Planescape: Torment startup script:
      gemrb/GUIScripts/pst/Start.py

    Baldur's Gate 2 inventory:
      gemrb/GUIScripts/bg2/GUIINV.py

    Shared inventory logic:
      gemrb/GUIScripts/InventoryCommon.py

GemRB API is provided to the scripts by means of python modules. They are implemented together with Python interpreter glue in GUIScript plugin in gemrb/plugins/GUIScript/GUIScript.cpp.

  Example:
    import GemRB
    GemRB.LoadTable ("RACES")

Description of all functions in the huge GemRB module is available [[guiscript:index|here]], GUIScript.cpp file itself or obtained by typing help(GemRB) in the GemRB console (press ctrl-space to open or close it). In that case the help is dumped to stdout.

All scripts to be run have to define OnLoad() function, which is called by GemRB each time the script is loaded. Those python files without OnLoad() function are merely modules imported into another script, usually MessageWindow.py.

The control between scripts is passed with the GemRB.SetNextScript() function, which causes another script to be loaded. Repeated loading of a script causes repeated calling of its OnLoad() function.

All scripts automagically import all symbols from file gemrb/GUIScripts/GUIDefines.py, which contains definition of constants commonly used in the scripts. Other files in this directory can be imported too.

  Example:

    import GemRB
    from ie_stats import IE_SAVEVSPOLY

    def OnLoad():
        print IE_GUI_BUTTON_NORMAL, IE_SAVEVSPOLY
        GemRB.SetNextScript ("MessageWindow")


===== Typical GUIScript =====

A typical script's task is to open a window, setting button labels and callbacks and then return control back to GemRB. The script also provides a mean to close the window created.

Since most scripts are modules of MessageWindow.py instead of independent scripts, we will use a simplified version of one of them for an example.

<code python>

    ## Standard header and boilerplate, put in all the scripts.

    # -*-python-*-
    # GemRB - Infinity Engine Emulator
    # Copyright (C) 2003 The GemRB Project
    .....

    ## Since this is only a module, GemRB and GUIDefines have to be explicitly
    ## imported

    ###################################################
    import GemRB
    from GUIDefines import *
    from GUICommon import CloseOtherWindow

    ###################################################
    JournalWindow = None
    LogWindow = None
    QuestsWindow = None
    ## Function called from a callback in MessageWindow.py

    ###################################################
    def OpenJournalWindow ():
        global JournalWindow

        ## This is a common way of closing another window before opening
        ## our own one (here Journal window). If Journal window already
        ## exists, it's closed and the control is returned to the caller
        ## (this implements toggling of a window)

        if CloseOtherWindow (OpenJournalWindow):
                ## this part is called if we are closing Journal window

                ## Close any opened children windows
                if LogWindow: OpenLogWindow ()
                if QuestsWindow: OpenQuestsWindow ()

                ## Freezes the GUI
                GemRB.HideGUI ()

                ## Free the window resource and delete Journal window
                ## from window manager
                JournalWindow.UnloadWindow ()
                JournalWindow = None
                GemRB.SetVar ("OtherWindow", -1)

                ## Unfreeze the GUI, redraws screen, reshuffles windows in
                ## window manager
                GemRB.UnhideGUI ()

                ## Return to the caller
                return

        ## This part is executed when we did not close Journal window
        ## and will open it instead

        ## Freeze the GUI
        GemRB.HideGUI ()

        ## Loads CHU resource file. This one contains windows for Journal,
        ## Log, and PC/NPC encyclopedia.
        GemRB.LoadWindowPack ("GUIJRNL")

        ## Load window with ID 0 (Main Journal window) from GUIJRNL.CHU file.
        JournalWindow = GemRB.LoadWindow (0)

        ## Add the new window to window manager. "OtherWindow" is name reserved
        ## for non-floating windows in the center of the screen.
        GemRB.SetVar ("OtherWindow", JournalWindow)

        ## Now access some buttons in the new window and assign
        ## labels and callbacks to them

        ## Comment the button processing with the label of the button for
        ## easy maintenance

        # Quests

        ## Quest button has ID 0 in GUIJRNL.CHU in window ID 0
        Button = JournalWindow.GetControl (0)

        ## Set the label for the button to strref 20430 ("Quests")
        Button.SetText (20430)

        ## When the button is clicked by mouse, call fn OpenQuestsWindow()
        Button.SetEvent (IE_GUI_BUTTON_ON_PRESS, OpenQuestsWindow)

        ## Now some other buttons. Note that "Done" button calls this
        ## function, which causes closing of the Journal window

        # Beasts
        Button = JournalWindow.GetControl (1)
        Button.SetText (20634)
        Button.SetEvent (IE_GUI_BUTTON_ON_PRESS, OpenBeastsWindow)

        # Done
        Button = JournalWindow.GetControl (3)
        Button.SetText (20636)
        Button.SetEvent (IE_GUI_BUTTON_ON_PRESS, OpenJournalWindow)

        ## Unfreeze the GUI, redraw screen and restart window manager
        GemRB.UnhideGUI ()
</code>


===== GUIScripter's Workflow =====

  - Run GemRB and an original game in parallel and notice GUI parts missing or badly done in GemRB.
  - Divine the CHU file containing the window resource. It can either be guessed by comparing names of all CHU files in the game to required functionality, from GUIScript already implementing related part of the GUI or by viewing all CHU files in CHU/BAM/MOS viewer (DLTCEP, NearInfinity) and finding the right looking one.
  - Find ID of the window to be implemented in the CHU file, usually with DLTCEP or NearInfinity.
  - Copy & paste common parts of the script from another one opening similar windows. Use shared code where possible.
  - Using CHU viewer find control IDs for the controls you need to configure in the script and add their setting into the script, again copy & pasting from another script. Usually start with magic trinity of GetControl, SetText, SetEvent for each button. Use dummy numbers for now. Beware, NearInfinity displays wrong IDs for labels. You will have to add the label's control ID to "buffer size" << 32 to get the right control ID.
  - Write down button labels as they are seen in the original game and find their strrefs using DLTCEP or NI. Alternatively, you can set "Strref On=1" in *.INI of the original games and write down directly the strrefs as they are displayed instead of the labels. This however does not work with Planescape: Torment.
  - Put the strrefs into the SetText() functions in the script.
  - Now that the trivial parts are done, the real fun can begin. But that's a subject too advanced for this page. :-)


===== Notes =====

Please try to follow the indentation style of other GUIScript scripts, in particular:

  * indent each level with 1 TAB character
  * insert space between function name and opening parenthesis in function calls and around operators and after the hash sign in text comments
  * insert blank lines between logically separated chunks of code, e.g. between setting of different controls
  * insert blank lines after functions and a row of hashes
  * if a function does not return a value, do NOT end it with 'return'
  * keep naming convention for windows, callback etc.
  * comment your code
