ExtendingScriptCompilers         The compilers in Shoggoth have several mechanisms for customizing and extending them built in

The compilers in Shoggoth have several mechanisms for customizing and extending them built in. They take the form of custom translators and the listener interface.

Listener Interface

The new compilers can have listeners set on them which allows for information gathering during compilation and also some level of customization. They will return information as scripts are compiled which allows you to better monitor the process, as well as provide custom hooks to change allocation and naming strategies on-the-fly.

virtual ConcreteNodeListPtr importFile(ScriptCompiler *compiler, const String &name);
         
 virtual void preConversion(ScriptCompiler *compiler, ConcreteNodeListPtr nodes);
         
 virtual bool postConversion(ScriptCompiler *compiler, const AbstractNodeListPtr&);
         
 virtual void handleError(ScriptCompiler *compiler, uint32 code, const String &file, int line, const String &msg);
         
 virtual bool handleEvent(ScriptCompiler *compiler, const String &name, const std::vector<Ogre::Any> &args, Ogre::Any *retval);
         
 virtual Ogre::Any createObject(ScriptCompiler *compiler, const String &type, const std::vector<Ogre::Any> &args);
  • importFile - This function is called when the "import" command is used from within a script. This function is expected to find the specified file name and return its concrete syntax tree. You can obtain a CST by using the ScriptLexer and ScriptParser classes to build one from raw text. You can also defer importation to the regular Ogre resource system by returning an empy CST like this



return ConcreteNodeListPtr();
  • preConversion - This function is called just before the compiler transforms the concrete syntax tree into an abstract syntax tree (a process which compresses the input by using context)



  • postConversion - This function is called just after the transformation to AST occurs. The AST can be modified within this function to customize the compilation of the script. You can also use this opportunity to preprocess any custom additions to the scripts, or to gather information. You can abort further processing by returning false.

  • handleError - This function is called when a compilation error has occured somewhere in the pipeline. Error information is provided. Errors are stored in the compiler itself for later queries as well.

  • handleEvent - This is the generic compiler event handler. This is called duration compilation to allow for customization of script processing. The built-in compilers (material, particle, and compositor) use the following events as customization points
    • preApplyTextureAliases - This called after a material has been compiled and before the texture aliases are applied to it.
      • arg1 - A Material* pointing to the compiled material
      • arg2 - A AliasTextureNamePairList* pointing to the map of texture aliases that will be applied
      • return Nothing
    • processTextureNames - This is called whenever a texture name is being used as an argument for a script property. This allows you to modify and decorate names at will on-the-fly.
      • arg1 - A String* that is an array of the texture names being processed
      • arg2 - An int that is the number of texture names in the array
      • return Nothing
    • processMaterialName - This called whenever a material name is being used as an argument for a script property.
      • arg1 - A String* pointing to a single String material name
      • return Nothing
    • processGpuProgramName - This called whenever a gpu program name is being used as an argument for a script property.
      • arg1 - A String* pointing to a single String gpu program name
      • return Nothing
    • processNameExclusion - This called by the AST conversion routine to allow for customizing name exclusions. Most objects (material, vertex_program, particle_system) require or allow for the use of a name. Some objects however are forbidden from using a name because the location for the name is used by another property (particle emitters and affectors have there types where the name would be). Handling this function lets you customize what objects are allowed names and which aren't. Only objects which can have names can use name-based inheritance overriding.
      • arg1 - A String that is the class name of the object (e.g. "material", "compositor", or "affector").
      • arg2 - AbstractNode* that points to the object's parent. This allows you to analyze the object's context to better determine if it should be name exluded.
      • return A boolean true or false. True means it is name excluded (not allowed to have a name), false means it is not excluded.

  • createObject - This called whenever the compiler needs an Ogre resource object to be allocated. This lets you override allocation strategies, naming, or the parameters used to construct the resource. The built-in compilers use this to allocate material, compositor, and particle system resources. The type argument tells you what kind of resource is being requested. Returning an empty Any object will cause these built-in compilers to use default strategies for creating these objects.
    • Material - Allocate a new material
      • arg1 - String that is the name of the file being compiled
      • arg2 - String that is the name specified in the script file
      • arg3 - String that is the resource group the script is being compiled into
      • return A Material* pointing to the Material object to compile the script into
    • GpuProgram - Allocate a low-level gpu program
      • arg1 - String that is the name of the file being compiled
      • arg2 - String that is the program name specified in the script file
      • arg3 - String that is the resource group
      • arg4 - String that is the specified source file
      • arg5 - GpuProgramType specified in the script
      • arg6 - String that is the program's specified syntax code
      • return A GpuProgram* pointing to the GpuProgram object that will be compiled
    • UnifiedGpuProgram - Allocate a new unified gpu program
      • arg1 is a string holding the file where the object is compiled from
      • arg2 is the name for the material specified in the script file
      • arg3 is the resource group of the compiling script file
      • arg4 is the GpuProgramType
      • return A HighLevelGpuProgram* pointing to the HighLevelGpuProgram object that will be compiled
    • HighLevelGpuProgram - Allocate a new high-level gpu program
      • arg1 is a string holding the file where the object is compiled from
      • arg2 is the name for the material specified in the script file
      • arg3 is the resource group of the compiling script file
      • arg4 is the language specified for the gpu program
      • arg5 is the GpuProgramType
      • arg6 is the specified source file
      • return A HighLevelGpuProgram* pointing to the HighLevelGpuProgram object that will be compiled
    • ParticleSystem - Allocate a new particle system
      • arg1 - String that is the name of the file being compiled
      • arg2 - String that is the name specified in the script file
      • arg3 - String that is the resource group the script is being compiled into
      • return A ParticleSystem* pointing to the ParticleSystem template to compile the script into
    • Compositor - Allocate a new compositor
      • arg1 - String that is the name of the file being compiled
      • arg2 - String that is the name specified in the script file
      • arg3 - String that is the resource group the script is being compiled into
      • return A Compositor* pointing to the Compositor object to compile the script into

Custom Translators

Writing a custom translators allows you to extend Ogre's standard compilers with completely new functionality. The same scripting interfaces can be used to define application-specific functionality. Here's how you do it.

The first step is creating a custom translator class which extends Ogre::Translator.

class TestTranslator : public Ogre::ScriptTranslator
{
private:
    int mValue;
public:
    TestTranslator();
    void translate(Ogre::ScriptCompiler *compiler, const Ogre::AbstractNodePtr &node);
};


This class defines the important function to override: translate. This is called when the TestTranslator needs to process a sub-set of the parsed script. The definition of this function might look something like this:

void TestTranslator::translate(Ogre::ScriptCompiler *compiler, const Ogre::AbstractNodePtr &node)
{
    ObjectAbstractNode *obj = reinterpret_cast<ObjectAbstractNode*>(node.get());
    
    for(AbstractNodeList::iterator i = obj->children.begin(); i != obj->children.end(); ++i)
    {
        if((*i)->type == ANT_PROPERTY)
        {
            PropertyAbstractNode *prop = reinterpret_cast<PropertyAbstractNode*>((*i).get());
            if(prop->name == "value")
            {
                if(!prop->values.empty())
                {
                    if(!getUInt(prop->values.front(), &mValue))
                        compiler->addError(ScriptCompiler::CE_INVALIDPARAMETERS, prop->file, prop->line);
                }
                else
                    compiler->addError(ScriptCompiler::CE_STRINGEXPECTED, prop->file, prop->line);
            }
        }
        else
        {
            processNode(compiler, *i);
        }
    }
}


The translate function here parses a single parameter, "value" and assigns it to the class variable mValue. Any number of properties can be parsed from in here and assigned to be processed later. Sub-objects can also be processed by checking if the child node type is ANT_OBJECT.

From here you need to register the translator to be invoked when the proper object is found in the scripts. To do this we need to create a factory object to create your custom translator. The factory looks like this:

class TestTranslatorManager : public Ogre::ScriptTranslatorManager
{
private:
    TestTranslator mTranslator;
public:
    TestTranslatorManager(){}
    size_t getNumTranslators() const{return 1;}
    Ogre::ScriptTranslator *getTranslator(const Ogre::AbstractNodePtr &node){
        Ogre::ScriptTranslator *translator = 0;
        if(node->type == Ogre::ANT_OBJECT)
        {
            Ogre::ObjectAbstractNode *obj = reinterpret_cast<Ogre::ObjectAbstractNode*>(node.get());
            if(obj->cls == "test")
                translator = &mTranslator;
        }
        return translator;
    }
};


Note that new translators are created here, just returned when requested. This is because our translator does not require separate instances to properly parse scripts, and so it is easier to only create one instance and reuse it. Often this strategy will work.

In order to register the factory with the core system you need only construct it and call the appropriate method on the ScriptCompilerManager like this:

TestTranslatorManager translatorManager;
Ogre::ScriptCompilerManager::getSingleton().addTranslatorManager(&translatorManager);


The order that custom translator managers are registered will make a difference. When the system is attempting to find translators to handle pieces of a script, it will query the translator managers one-by-one until it finds one that handles that script object. It is a first-come-first-served basis.

The script object that this system handles will look like this:

test
{
    value 10
}


An important note is that this will recognize the above pattern no matter where in the script it is. That means that this may appear at the top-level of a script or inside several sub-objects. If this is not what you want then you can change the translator manager to do more advanced processing in the getTranslator function.