Tutorial 2: Custom-Viewer

From WiCWiki

Jump to: navigation, search

Contents

by Insane Buzzstards

This is a tutorial about the Custom Viewer !

Questions and Annotations

This tutorial can be discussed in this thread. Please help us making this tutorial as perfect as it can be, by giving hints and writing comments!

Introduction

This is one half tutorial and one half tool-presentation. So what is this Custom Viewer??? Did you already take a look at WiC's Debug Viewer??? The Custom-Viewer is almost the same. It is a game-overlay with the help of which you can execute Python-Code whenever you like. The difference to the original debug-viewer is, that it is build for easier extendability, so that you can easily and efficiently add your python-code to the viewer. The viewer has almost no options in the beginning, but you can later create your own modules and entries, which you can easily share with all other modders via the WiC-Wiki. If such a module has a bunch of different options and entries, we will call this an editor. One example for an editor, could be the AreaEditor, with the help of which you can create and visualize areas in the landscape.

Here is a screenshot of how a very simple custom-viewer could look like:

custom_viewer.jpg

Look here for a list of already existing Custom-Viewer-editors.

In this tutorial, we won't teach you how to write your own custom-viewer, but explain how to extend our Custom-Viewer to fit to your needs. We will give you an overview of the structure and the methods you will have to use.

Resource downloading

You can download the py's of the custom-viewer here: [1]

Reading: Structure

So ... this chapter is titled "Reading: Structure" ... do what it says: just read and don't try anything. This is mostly just background. We will do something practical later, in the tutorial-part!

What is in this package?

  cviewer.py                       # your customized viewer
  cust_viewer/Entry.py             # sources for the viewer
  cust_viewer/misc.py              # some other stuff that might be needed
  input_handler/InputHandler.py    # code for handling user-input
  input_handler/keys_scancodes.py  # variables for most keys (for user-interaction)

Place the "cviewer.py" and the "cust_viewer" and "input_handler"-folder in a folder that is included by the WiC-Python-Includepaths, f. ex. in "python" or in your modfolder. To use the Custom-Viewer, you have to import cviewer and call cviewer.myViewer.activate() or cviewer.myViewer.toggleViewer(). To make things easier, you can add it to your "wicautoexec.txt" (in the WiC-folder of "My Documents" ... create it if it doesn't exist) and toggle the viewer via F2:

py import cviewer
bind F2 py cviewer.myViewer.toggleViewer()

Usage

Select entries with your left-mouse or navigate with the up- and down-arrow-key. To expand an entry or make it do what it says press "space".

Entries (module: "Entry" in package "cust_viewer")

Members

The Custom-Viewer is based on entries. An entry is simply a node in the list. An entry has the following members (some of them are not important to you):

  m_text                 = the main-text of the entry (string)
  m_preText              = additional text before main-text (string)
  m_postText             = additional text after main-text (string)
  m_childList            = child-entries (List<Entry>)
  m_childMap             = child-entries (Dictionary<String,Entry>), the key is the m_text of the child
  m_isExpanded           = is the entry expanded (boolean)
  m_isVisible            = is the entry visible (boolean)
  m_color                = the current color of the entry (int 0x)
  m_actions              = the actions of an Entry (Dictionary<String, List<Action>>), you can create groups of actions
  m_parent               = the parent-Entry (Entry), None for root-entries
  m_positionInParentList = the position in the parents entry list (int)
  m_tooltip              = a tooltip that will be shown to give information (String)

Creating Entries

When you create an entry, you mostly give the constructor 2 values: the text of the entry and its parent.

child = Entry("I am a child of my parent", parentEntry)
childChild = Entry("I am a child of child", child)

When you create an entry, the entry adds itself to the children of its parent. To be honest: it adds itself to a list of childs ("m_childList"), which is important for the order, in which the entries are printed, and a dictionary of childs ("m_childMap"), which is better for accessing the entries. In the dictionary, the key for an entry is its "m_text"-attribute (without pre- or post-text).

Instead of giving "child" as the parent of "childChild" you could also do the following:

child = Entry("I am a child of my parent", parentEntry)
childChild = Entry("I am a child of child", parentEntry["I am a child of my parent"])

This is helpful, when you create parents and children in different methods.

The Custom-Viewer itself is a class. Every Custom-Viewer has a root-entry, that is automatically created, which is myViewer.m_rootEntry. So the first entry you will add to a viewer will be a child of the rootEntry:

child = Entry("I am a child of the root-entry", myViewer.m_rootEntry)
childChild = Entry("I am a child of child", myViewer.m_rootEntry["I am a child of the root-entry"])

Note that myViewer is an instance of a custom-viewer. Such an instance will already be created for you in the cviewer-module.

Actions

You can add actions to entries and execute these actions whenever you want. What is an action? Actions are provided by WiC and are nothing but a way to store methods and execute those whenever you want. You create an action in the following way:

a = Action(nameOfMethod, para1, para2, para3)

Now you have stored an action and whenever you call "a.execute()", the action executes the method "nameOfMethod" with the parameters "para1", "para2" and "para3". You may think that this is pretty senseless, because you can call the method on your own, but in the end this is very useful, when having a list of methods you want to execute. You just have to iterate over the list and call action.execute() for every action.

As we said, you can add actions to entries, what is useful for reacting to user-events. The actions are stored in groups of lists. This makes it possible to react to different user-events. You could for example make a group of actions, that shall be executed when the user presses "g" and another when the user makes a double-click on the entry.

The method for adding an action to an entry takes 3 parameters: the entry, the action should be added to. The group, the action should belong to and the action itself:

entryAddAction(child, "use", Action(entryToggleExpand, child))

As you see, we add the method "entryToggleExpand" to the group "use" of the entry's actions. This method expands the entry if it is not already expanded and verse visa. We can execute all actions of a group by calling the Entry-method "executeActions(group)":

child.executeActions("use")

Making this call will expand or unexpand "child". Later we will teach you, when you will execute an entry's actions.
There are two actions that an entry will automatically create for itself, namely toggling its color when it is selected or deselected. So the two groups "select" and "deselect" already exist.

NOTE: entryToggleExpand is already deprecated, but it serves as a good example! If you want to create an expandable entry, use the entries "makeExpandable(isExpanded)"-method instead, this will automatically add entryToggleExpand to the "use"-group.

The CustomViewer (module: "CustomViewer" in package "cust_viewer")

The viewer itself just has a few methods:

  activate()                        # activates the viewer
  deactivate()                      # deactivates the viewer
  toggleViewer()                    # toggles the activation-state of the viewer
  update()                          # the update-method
  initViewer()                      # initialize the viewer by creating the entries
  printViewer()                     # prints the viewer and its entries
  setSelectedEntry(entry)           # sets which entry should be selected
  getSelectedEntry()                # gets the selected entry
  setPreviousSelectedEntry(entry)   # sets which entry was previously selected - you won't need this one
  getPreviousSelectedEntry()        # gets the previously selected entry
  selectEntryByPosition(xPos, yPos) # selects an entry by given coordinates (used for selecting entries by mouse)
  selectNextEntry()                 # selects the next entry (goes downwards in the list)
  selectPreviousEntry()             # selects the previous entry (goes upwards in the list)

Those should be clear.

Your customized Viewer (module "cviewer")

update()

The update-method is called for every frame. It checks the user-input and redaws the viewer and everything else you want to draw.

user-interaction

There are already parts, f.ex. for navigating with the arrow-up and arrow-down-key and handling left-mouse-clicks and left-mouse-double-clicks, as well as handling the "space"-key. The logic is quite simple. You can press a key (keyboard or mouse) in three different ways: you press it once and want a single action. you press it twice for another action (f.ex. mouse-double-click) or you hold it to repeat an action until you release the key. So there are three different methods given in the "InputHandler", which you will use.

# checking if a key was pressed and execute an action just once
def checkSinglePress(key, action)
   # key    = the key, that shall be checked
   # action = the action, that shall be executed
# checking  if a key was pressed, executing an action just once and check if double-press and execute second action once
def checkDoublePress(key, singleAction, doubleAction, dblPressTime=0.2)
   # key          = the key, that shall be checked
   # singleAction = the action, that shall be executed on a single press, give "None" if you just want double-click-action
   # doubleAction = the action, that shall be executed on a double press
   # dblPressTime = how much time can be between the first and second press to execute the double-press-action, default is 0.2s
# checking if the user holds a key and execute it
def checkHoldingKey(key, action, timeFirst=0.4, timeNext=0.03)
   # key          = the key, that shall be checked
   # action       = the action, that shall be executed
   # timeFirst    = how much time should pass, until the second execution can follow, default is 0.4s
   # timeNext     = how much time should pass, until the next execution can follow (except the second), default is 0.03s

There are also methods that do the same as those, just for multiple keys at the same time. For an overview over all keyboard-keys, look into the "keys_scancodes.py" . Mouse-keys are in the "InputHandler.py"

Keyboard-Key-Example

As you can see we have to give an action to these methods. So when you want to add a key, you have to write a method that describes what the key should do. Here is an example for the space-key. You can create every other key in the same way:

# the method that will be called when "space" is pressed
def key_act_SPACE_single():
    selectedEntry = myViewer.getSelectedEntry()
    selectedEntry.executeActions("use")

And in the "update()"-method:

checkSinglePress(KEY_SPACE, Action(key_act_SPACE_single))

In this example we check if the key was pressed once and we want to have a single-action. Always declare the methods in this format "key_act_IDENTIFIER_TYPE". An identifier is the name of the key, f. ex. "KEY_SPACE", just without "KEY", so just "SPACE". The TYPE is the way in which the key is pressed. Use "single" for single presses, "double" for double-presses and "hold" for holding the key. The format is important, as different editors might use the same key for different actions for their entries (what is perfectly possible).

As you can see, we check whether the user presses space and if so we get the selectedEntry and execute all its "use"-actions, f.ex. expanding it. This is the place for executing the entries' actions. But you can do whatever you want in these methods - there's no need that it has something to do with the custom-viewer or the entries.

NEVER use two of the check-methods together for one key. that makes no sense, except you are able to control the behaviour. So if you want to have a single and a double-press, just use the double-press-method.

Mouse-Key-Example

Another thing: if you are writing a method for your mouse-keys, the method needs to have 2 Parameters for the mouse-coordinates, even if you don't use those. Otherwise the game will crash. Here is an example for the mouse-left-key:

# this method will be called when the user clicks once with the left-mouse-button
def key_act_MOUSE_LEFT_single(posX, posY):
    # do stuff here
# this method will be called, when the user makes a double-click
def key_act_MOUSE_LEFT_double(posX, posY):
    selectedEntry = myViewer.getSelectedEntry()
    selectedEntry.executeActions("use")

and in the "update()"-method:

# check for left-mouse-button-interaction, different actions for single- and double-click
checkDoublePress(KEY_MOUSE_LEFT, Action(key_act_MOUSE_LEFT_single, 0, 0), Action(key_act_MOUSE_LEFT_double, 0, 0))

When declaring the actions, you have to give the mouse-coordinates as zeros or whatever you like. Those will be filled with the correct coordinates later!

Printing/Drawing

At the end of the update-method you will also do all the drawing, so this method is also the place to execute "myViewer.printViewer()"

myViewer

The cviewer.py is also the place, where the Custom-Viewer is created. We simply declare a variable:

myViewer = CustomViewer(update)

It is important to give the viewer a function-pointer to the "update"-method that it will use. So simply give the name.

Tutorial: Creating a simple Area-Editor

After having read all this boring stuff, we want to do something productive. We will create an Area-Editor. This is an entry named "Area-Editor", having a child named "Add Area" and when you add an area, it will be visible as a child of the "Area-Editor". We won't just add those areas visually - we will add real areas, that you can use for other stuff later.

First of all you have to create a new file (python-module) in "cust_viewer" named "areaeditor.py".

Because we need to have access to our Custom-Viewer, we will create a viewer-variable and initialize it with None.

theViewer = None

We will begin by creating the "initializeAreaEditor"-Method. This one will create the first entries. When calling it, we will give it the Custom-Viewer, so that the method knows, to which viewer it should add the entries.

def initializeAreaEditor(aViewer):
    # set the viewer
    global theViewer
    theViewer = aViewer
    # create the root-Entry
    eAreaEditor = Entry("Area-Editor", theViewer.m_rootEntry)
    # make it expandable
    eAreaEditor.makeExpandable(False) # False means not expanded in the beginning
    
    # create the add-area-entry
    eAddArea = Entry("Add Area", eAreaEditor)
    # call the "addArea" method when someone "uses" this entry (hitting space)
    entryAddAction(eAddArea, "use", Action(addArea))

I think, the comments should explain everything. "makeExpandable(isExpanded)" internally executes "entryAddAction(eAreaEditor, "use", Action(entryToggleExpand, eAreaEditor))". So, as you can see both entries have different "use"-actions (when the user hits space, the "use"-actions of the currently selected entry will be executed.). The root-entry has a use-action to expand and unexpand it and the "addArea"-Entry has an action to call the method "addArea".

Let's create the "addArea"-Method!

We will have a Dictionary for our added areas and we will save the number of areas, that we have:

# the areas that are added using the viewer (Dictionary<String, Area>)
addedAreas = {}
  
# the number of added areas
numAddedAreas = 0

And here is the method.

def addArea():
    """
    Adds an ara
    """
    global numAddedAreas
    global currentArea
    
    # the name has to be unique, so just add the num of areas
    areaName = "Area" + str(numAddedAreas)
    
    # create the area at the mouse-position
    pos = getMouseToScreen()
    
    # create the area
    area = Area(pos, 10)
    
    # add it to the Dictionary and increase the num of areas
    addedAreas[areaName] = area
    numAddedAreas = numAddedAreas + 1
    
    # create an entry for the area and its actions
    entry = Entry(areaName, theViewer.m_rootEntry["Area-Editor"])
    entry.makeExpandable(False)

As you see, we create a unique name for the entry/area that will be added. We will create the area at the current mouse-position. This is a method from the "misc"-module. If the mouse-position is invalid, than the area will be created at the camera-lookat. After that we add the area to our dictionary, so that we are able to access it from outside (f.ex. for creating units in this area or something). Last but not least: we create the entry for the area. Note that the parent is the "Area-Editor"-entry. We also add an action for expanding (which is still quite useless, because the entry has not childs).

Well ... we still missed some imports before that method, otherwise you will get errors:

from cust_viewer.misc import getMouseToScreen
from cust_viewer.misc import drawCircle
from cust_viewer.Entry import Entry
from cust_viewer.Entry import entryAddAction
  
from area import Area
from reaction.action import Action
  
import wicp
import serverimports as si

When you look at the imports, you will see that we use "cust_viewer" right before the module we want to load. You can do this, because the folder "cust_viewer" is treated as a python-package.
Last but not least, we will write a method to print the areas in the world:

#the color areas should be drawn in
COLOR_AREA = 0x00ff00

def printAreas():
    """
    Prints the Areas in the world.
    """
    for ar in addedAreas:
        # get the screen-position of the name
        worldPosName = addedAreas[ar].myPosition
        screenPosName = wicp.World2Screen( worldPosName )
        # if is visible, draw it
        if not screenPosName is None:
            si.ClientCommand( 'PrintText', ar, screenPosName[0], screenPosName[1], COLOR_AREA )
        
        # draw a circle to visualize the area
        drawCircle(addedAreas[ar].myPos, addedAreas[ar].myRadius, COLOR_AREA)

For all areas in our dictionary, we get the position of that area, calculate the according screenposition, draw the name and draw a circle. The drawCircle-method is from the "misc"-module.

Now our areaeditor-module is ready. There are still two things that we have to do.

We have to initialize the AreaEditor in the cviewer's "initViewer()"-method. Therefore we have to import the necessary methods first:

from cust_viewer.areaeditor import initializeAreaEditor
from cust_viewer.areaeditor import printAreas

Then add "initializeAreaEditor(myViewer)" to your cviewer's "initViewer()"-method.
Also, we want to print out the areas every frame. So you have to add "printAreas()" at the end of the cviewer's "update()"-method.

That's it! you successfully created a simple Area-Editor!!! Now you should know, how to work with the Custom-Viewer to make your modder-life easier.

Sharing Editors

Of course we encourage you to share your editors. Therefore make a page for your Editor and add this page to the Editor-List of the Custom Viewer
On your editor's page, add a download-link to your Python-Script and explain how the "cviewer.py" has to be changed, so that it uses your editor.
Look the AreaEditor's page for an example.

Personal tools
User Created Content