GUI Scripting: Inventory Icon Example

From The DarkMod Wiki
Jump to navigationJump to search

This is a part of a series, whose hub is GUI Scripting Language

Introduction to Inventory GUI Customizaton

This example is a follow-on from the discussions in Inventory about "Custom HUD" and particularly about "How to make inventory items interact with world objects" by Kingsal. That latter topic introduced a skull, as a "Custom Item", with a frob script to let it to be easily placed on a pedestal altar. That example used the default inventory item GUI. Here, a customized and renamed version of GUI is considered. Its purpose is to make the yellow skull, when it's the current inventory item, pulse with a blue tint. (This will not affect how the skull appears in the inventory grid, nor as a 3D object in-game.)

Notable aspects covered here:

  • Global and local GUI::Parameters to pass values
  • onTime loop with transition for color change
  • An inventory script object template, generally useful for custom inventory GUIs.

In addition, an alternative method, where the default inventory item GUI is simply overridden, is discussed.

These examples, and the compass example in Inventory, should help with your own customizations.

The Default GUI

Most inventory items use the default GUI, developed by greebo and found at tdm_gui01.pk4/guis/tmd_inv.gui. It's a bit lengthy, but here's the skeleton.

Structural Outline

// Useful #defines here, shown further below.
windowDef Desktop {
    windowDef InventoryIcon {
       windowDef InventoryNegativeFeedback {...} // red glow
       windowDef InventoryPositiveFeedback {...} // green glow
       windowDef InventoryGroupText {...} // category name
       windowDef InventoryItemIcon {...} // icon                 <== What will be customized!!!
       windowDef InventoryItemName {...} // Name (after any translation)
       windowDef InventoryItemName2 {...} // if 2-line name (after translation)
       windowDef InventoryItemCount {...} // For stackable items

   onNamedEvent onInvNegativeFeedback {...} // Called by SDK code
   onNamedEvent onInvPositiveFeedback {...} // ditto
   // To display notifications like "Picked up 250 loot":
   windowDef InventoryPickUpMessage {...}
   onNamedEvent DisplayInventoryPickUpMessage {...}
   onNamedEvent SetupInventoryPickUpMessageSystem {...} // Called at player spawn time

   // Which item currently grabbed/equipped by player:
   windowDef GrabbedItemNameText {...} 

   // If player is shouldering a body:
   windowDef InvOccupiedIcons {
       windowDef InvOccupiedIconRight // icon of right-hand-occupied
       windowDef InvOccupiedIconLeft // icon of left-hand-occupied
       onNamedEvent OnStartShoulderingBody {...} // Called by SDK when player shouldering body
       onNamedEvent OnStopShoulderingBody {...} // Called by SDK when un-shouldering body

GUI::Parameters Used

Standard Globals

TDM's Settings/Video/Advanced has an item (HUD Size/Opacity) that allows a gamer to interactively alter how the HUD appears. This changes CVar values, and in turn those values are made into "global GUI::parameters" by SDK calls, so that every GUI that is part of the overlay system has immediate access to them. (For more about HUD Size/Opacity, see GUI Scripting: Getting System CVars.) Consequently, a well-designed inventory GUI can read these and appropriately adjust the appearance of the inventory item's icon and text. The default GUI mainly incorporates them into #defines, which allows them to be more compactly referenced:

#define S1 "gui::iconSize"
#define S2 S1*"gui::smallTextSize"
#define S5 "gui::bigTextSize"
#define S4 "gui::lightgemSize"
#define S3 S4*"gui::barSize"
#define INV_GUI_FONT_SCALE 0.16*S2
#define INV_GUI_FONT "fonts/stone"
#define INV_TEXT_SETTINGS visible "gui::Inventory_ItemVisible" noclip 1 matcolor 1, 1, 1, "gui::HUD_Opacity" \
   textalign 1 textscale INV_GUI_FONT_SCALE font INV_GUI_FONT forecolor 1, 1, 1, "gui::HUD_Opacity"

In the above, all the gui:: parameters except "gui::Inventory_ItemVisible" are globals.


Local GUI::Parameters allow private communication between a GUI and either a .script or .cpp function, using the GUI handle (a specific integer) as a communications channel. Specifically, the default inventory GUI is driven by the engine (in Player.cpp code) using these gui::parameters and a standard inventory HUD handle:

  • Inventory_ItemVisible bool
  • Inventory_GroupVisible bool
  • Inventory_ItemIcon string
  • Inventory_ItemGroup string Aka Category
  • Inventory_ItemNameMultiline bool
  • Inventory_ItemName string
  • Inventory_ItemName_2 string
  • Inventory_ItemStackable bool
  • Inventory_ItemCount float as int

Why are these of interest? Because gui::parameters with the same names will be used in our GUI customization. While fresh names could have invented, using the existing names means that the custom GUI file will need minimal changes from the default GUI file. There will be a different handle, though, so the custom set of gui::parameters is actual distinct from the set of gui::parameters involved with the standard inventory HUD handle. Each handle has its own "dictionary" of defined local parameters.

Customizing a Copy of the Default GUI

Make a copy of tdm_inv.gui, called, say, "tmd_inv_pulse.gui". Place that in your <FM>/guis/ folder. Here's the part of the default GUI of interest:

       windowDef InventoryItemIcon {
           visible "gui::Inventory_ItemVisible"
           rect 35*S1, 35*S1, 50*S1, 50*S1
           background "gui::Inventory_ItemIcon"

To that, add a temporal loop of onTime handlers with transition statements, giving:

       windowDef InventoryItemIcon {
           visible "gui::Inventory_ItemVisible"
           rect 35*S1, 35*S1, 50*S1, 50*S1
           background "gui::Inventory_ItemIcon"

           // "Pulse" feature. Momentarily reduce red & green channels to make blue tint
           onTime 0 {
               transition "matcolor" "1 1 1 0.7" "0.5 0.5 1 0.7" 200;

           onTime 400 {
               transition "matcolor" "0.5 0.5 1 0.7" "1 1 1 0.7" 200;

           onTime 800 {
               resetTime "InventoryItemIcon" 0;

Invoking the Custom Inventory GUI in the entityDef

If you followed the procedure In Inventory, for your skull object you created an entityDef, with either the default name "inventory__use_item" or some other name of your choosing. That's saved in some file, like <FM>/def/custom.def. That sample entityDef had this section:

//Inventory stuff
  "inv_name"       "custom item name" //Rename it something fun! MUST match the name on the Altar.
  "inv_icon"       "guis\assets\hud\inventory_icons\skull_inv_icon" // GUI icon.
  "inv_droppable"  "1" //Allows the player to drop this. Beware they might drop it someplace where it can't be retrieved. 
  "inv_category"   "Custom Items" //Rename this to the appropriate category.
  "inv_stackable"  "0" //This is only needed if there are multiple of these items that you want to stack in the inventory.

Now add to it 2 lines:

  "inv_hud"        "guis/tdm_inv_pulse.gui"
  "inv_hud_layer"  "11" // greebo: Readables are on layer 10

The first points to our custom gui. The second puts it on a render-layer that's behind any readable. (Note that that the current inventory icon with the default GUI is shown in render-layer 2. That works because, when a readable is open, there's special handling in code to hide the inventory icon. That special handling is not available with a custom GUI.)

If you compiled and ran this FM as is, you might be surprised to find no skull icon or accompanying text shown. That's because our work isn't done... when you add a Custom Inventory GUI, you must also supply a script object. So add this line to your .def too:

  "scriptobject"   "custom_gui_item"

A Template for a Custom Inventory Script Object

There is no default .script file for this in the TDM distribution. That's because, as mentioned earlier, the default GUI relies on SDK calls rather than a script object. See Player.cpp's UpdateInventoryHUD() for that. So here, an equivalent default script object, "custom_gui_item", is provided.

It starts with the object declaration, using the outline with greebo's annotations, as described in Inventory. It declares a generic object, unlike the compass example, that inherits from player_tools. Recall that the compass uses a special separate thread to do an update each frame, responsive to player orientation. That's not needed here. The gui::parameters that we pass will have largely static values.

In the declaration, the last formal argument of Inventory_item_init(...) is renamed to "guiFile", to describe it more accurately. (A weird feature of the .script language is that the actual function signature must also use the same formal argument name.) When these functions are called, the first argument, "userEntity", will be player1, not the skull. Player1 owns the HUD.

object custom_gui_item {
     void init();

     // greebo: Gets called upon addition to the inventory
     void inventory_item_init(entity userEntity, float overlayHandle, string guiFile);	

     // Gets called when the player switches to another inventory item
     void inventory_item_unselect(entity userEntity, float overlayHandle);

     // Gets called when this item is selected in the inventory
     void inventory_item_select(entity userEntity, float overlayHandle);

     // Gets called to update the HUD (not each frame!)
     void inventory_item_update(entity userEntity, float overlayHandle);

void custom_gui_item::init() {
     // sys.println("custom_gui_item::init");

void custom_gui_item::inventory_item_init(entity userEntity, float overlayHandle, string guiFile) {
     // string msg = "inventory_item_init called with handle " + overlayHandle + " for " + guiFile; sys.println(msg);

Then, the actual functions of most interest. In "select" and "unselect", our custom GUI's content is respectively shown (when the item is the current selection) and hidden (when not):

void custom_gui_item::inventory_item_select(entity userEntity, float overlayHandle) {
     // string msg = "inventory_item_select called with handle " + overlayHandle; sys.println(msg); 
     userEntity.setGuiFloat(overlayHandle, "Inventory_ItemVisible", 1); 
     // Group/Category is always (momentarily) shown:
     userEntity.setGuiFloat(overlayHandle, "Inventory_GroupVisible", 1); 
     // NOT REALLY NEEDED: Disable the default inventory
     //userEntity.setGuiFloat(userEntity.getInventoryOverlay(), "Inventory_ItemVisible", 0);

void custom_gui_item::inventory_item_unselect(entity userEntity, float overlayHandle) {
     // string msg = "inventory_item_unselect called with handle " + overlayHandle; sys.println(msg);
     userEntity.setGuiFloat(overlayHandle, "Inventory_ItemVisible", 0);
     // Not required by GUI, if ItemVisible is 0:
     // userEntity.setGuiFloat(overlayHandle, "Inventory_GroupVisible", 0); 
     // NOT REALLY NEEDED: Enable the default inventory again
     //userEntity.setGuiFloat(userEntity.getInventoryOverlay(), "Inventory_ItemVisible", 1);

Because this item has a custom HUD, all of Inventory_... gui::parameters with respect to the standard inventory overlay stay at their default values of zero or empty string. And the HUD space in the lower right corner is empty, available to fill with custom content. As discussed earlier, you can independently re-purpose the GUI::parameter names (without interference) as long as you reference the overlayHandle, not the standard inventory overlay.

Finally, the "update" handler, next. It has the majority of the functionality needed to drive the GUI through the GUI::parameters. As stated above, this scripting seeks to emulate what the C++ code does for the default inventory GUI. Overall, "update" transfers data from player1's internal current inventory item structure to the GUI::parameters on our custom inventory handle. Some additional processing is needed. For instance, the item name (after translation) may be on 2 lines. (The category name is assumed to be still on one line, and all string values are assumed to be less than 128 characters, the max that setGuiString(...) can handle.)

void custom_gui_item::inventory_item_update(entity userEntity, float overlayHandle)
     string s;
     // string msg;
     float newline_index;
     s = userEntity.getCurInvIcon();
     userEntity.setGuiString(overlayHandle, "Inventory_ItemIcon", s); // assume s < 128 chars

     s = userEntity.getCurInvCategory(); // aka Group. Always only 1 line
     //msg = "update: group name before translate = " + s; sys.println(msg);
     if(strLeft(s, 5) == "#str_")
           s = translate(s);
     userEntity.setGuiString(overlayHandle, "Inventory_ItemGroup", s);
     //msg = "update: group name after translate and null check = " + s; sys.println(msg);

     s = userEntity.getCurInvItemName();
     if(strLeft(s, 5) == "#str_")
           s = translate(s);
     // Is there a line break in the name, possibly added by translator?
     newline_index = sys.strFind(s, "\n",0,0,-1); // last 3 args: casesensitive: 0; start: 0; end: -1
     // Return the position of the given substring, counting from 0, or -1 if not found.
     string name1, name2; // temp strings for benefit of println below
     if(newline_index != -1) {
         // sys.println("two lines");
         setGuiInt(overlayHandle, "Inventory_ItemNameMultiline", 1 );
         name1 = strLeft(s, newline_index);
         userEntity.setGuiString(overlayHandle, "Inventory_ItemName", name1 );
         name2 = strMid(s, newline_index + 1, strLength(s)-newline_index-1 );
         userEntity.setGuiString(overlayHandle, "Inventory_ItemName_2", name2 );
     } else {
         // sys.println("only 1 line");
         userEntity.setGuiInt(overlayHandle, "Inventory_ItemNameMultiline", 0 );
         name1 = s; // assume name1 < 128 chars
         userEntity.setGuiString(overlayHandle, "Inventory_ItemName", name1 );
         name2 = "";
         // userEntity.setGuiString(overlayHandle, "Inventory_ItemName_2", name2); // not really required
     //msg = "update: item name line 1 after translate and checks for null & line breaks = " + name1; sys.println(msg);
     //msg = "update: item name line 2 = " + name2; sys.println(msg);

A shortcoming of that last function is that, for stackable items, it can't get the item count from the SDK. TDM 2.11 will likely correct that, with something along these lines:

     float count = userEntity.getCurInvItemCount(); // New TDM 2.11. If non-stackable, returns -1, count of 1 assumed
     if(count > -1) {
            setGuiInt(overlayHandle, "Inventory_ItemStackable", 1 );
            setGuiInt(overlayHandle, "Inventory_ItemCount", count );
     } else {
            setGuiInt(overlayHandle, "Inventory_ItemStackable", 0 );
            setGuiInt(overlayHandle, "Inventory_ItemCount", 1 ); // not strictly required

See bug tracker 6096 for more.

An Alternative Method - Default Inventory GUI Override


Instead of creating a custom GUI that affects only the special entity, you could override the default inventory GUI. That is, put a copy of tdm_inv.gui without changing the name in your <FM>/guis/ folder, and then add your customizations (e.g., the transitions given above). Unlike the other method, DON'T set these entityDef properties:

  • inv_hud
  • inv_hud_layer
  • scriptobject

This override would affect ALL inventory icons in your FM that relied on the default. That may be very desirable for some purposes.

Advantages of this approach:

  • No custom script object is needed; just rely on the standard SDK processing
  • This method supports showing the count of a stackable item, where the other method (as of TDM 2.10) does not yet.

Applying this Method Narrowly

You might want to further change the override GUI, so that your customizations apply just to the desired item(s). That will be easier if the customizations are not extensive.

You need some way for your GUI to know which items are customized. Unfortunately, doing a string check of Inventory_ItemName or Inventory_GroupName (within a GUI if-statement) is unreliable due to possible language translations by the SDK. As a solution, TDM 2.11's SDK will (likely - see bug tracker 6096) propagate an existing optional spawnarg (inv_item_id) through a new GUI::Parameter, "Inventory_ItemId".

Here's how you would use it. Suppose you want the customization (color-pulse in this case) to apply to only 1 type of entityDef (the skull). Then you need to tag that entityDef with some made-up ID:

"inv_item_id" "pulser"

The SDK will automatically pass this (without translation) to the customized default GUI through GUI::Parameter "Inventory_ItemId". So change the GUI to look for that:

      windowDef InventoryItemIcon {
          visible "gui::Inventory_ItemVisible"
          rect 35*S1, 35*S1, 50*S1, 50*S1
          background "gui::Inventory_ItemIcon"

          // "Pulse" feature. Momentarily reduce red & green channels to make blue tint
          onTime 0 {
              if("gui::Inventory_ItemId" == "pulser") {
                  transition "matcolor" "1 1 1 0.7" "0.5 0.5 1 0.7" 200;

          onTime 400 {
              if("gui::Inventory_ItemId" == "pulser") {
                  transition "matcolor" "0.5 0.5 1 0.7" "1 1 1 0.7" 200;

          onTime 800 {
              resetTime "InventoryItemIcon" 0;