Core Architecture: Major System: Code Generation - UA-ScriptEase/scriptease GitHub Wiki

Summary

  • Code Generation generates game code based on the specified Translator and the constructed Story.
  • It is multi threaded, so you cannot use anything static in any of the classes.
  • Here are some of the biggest subsystems and a brief explanation of their purpose in Code Generation: Language Dictionary: defines the language specific format of the code which should be output by code gen
    • API Dictionary : provides resolution values to ScriptEase constructs. Things like Types, DoIt code, and Slots are taken from here.
    • Context System : the context system provides code gen with a reference point to requests in Language Dictionary and DoIt code. For example: a request for a "name" or "children" mean something different depending on the context (scope) of the call.
    • Fragments : pieces of formatted code to be resolved and inserted into the outputted game code. Code Generation breaks down Language Dictionary into these format fragments based on context (scope), lines, indents and other keywords.
    • Game Module : specifies how to deal with the outputted game code (where to write it, save it, ...) among other things.

How It Works

  1. Start Code Generation is started by calling generateScriptFile(Set rootBag, Translator translator) which grabs all of the CodeBlocks(subject, slot, code) from the model tree and the selected Translator.
  2. Semantic Check SemanticAnalyzer then processes the Story Roots with a StoryVisitor, and determines any errors within the Story Tree. If this succeeds then code generation continues.
  3. Setup generateScript(Context context) is then called with the initial Context created by SemanticAnalyzer. This starts the initial format resolution on the Language Dictionary defined "File" format which will recursively resolve until the code for the File has been generated.
  4. Format Resolution FormatFragment's resolveFormat(Collection format, Context context) and resolve(Context context) methods are called recursively until there are all fragments have been resolved. This resolution is determined by the type of Fragment (eg. SimpleFragment, ScopeFragment, SeriesFragment, LiteralFragment, ...), the context in which the fragment resides, and the dataLabel (desired information) requested by the Language Dictionary author in that Fragment.
  5. Compile and Save Upon completion of Fragment resolution, If the CodeGeneration had any errors, the compilation step is skipped and the user is informed. The module is then saved with the optional compile step.

Fragments and Context Resolution

Fragments and Contexts are the key components of code resolution, they specify which value is desired and where to place it in the code. Fragments are the little bits of data or processing that ScriptEase needs to do, while Contexts are descriptions of where that data comes from.

The following is a snapshot of SimpleFragment#resolve(...):

public String resolve(Context context) {
      super.resolve(context);
      final String dataLabel = this.getDirectiveText();

      // IF+ELSE BLOCK (fragment data = <dataLabel>)
      if (dataLabel.equals(TranslatorKeywordManager.XML_NAME_FORMAT))
         return context.getUniqueName(this.legalRange);
      else if (dataLabel.equals(TranslatorKeywordManager.XML_TYPE_FORMAT)) {
         String type = context.getType();
         if (type.equals(Context.UNIMPLEMENTED) && !defaultText.isEmpty())
            return defaultText;
         else
            return type;
      } else if (dataLabel.equals(TranslatorKeywordManager.XML_CODE_FORMAT))
         return context.getCode();
      else if (dataLabel.equals(TranslatorKeywordManager.XML_VALUE_FORMAT)) {
         return context.getValue();
      } else if (dataLabel
            .equals(TranslatorKeywordManager.XML_CONDITION_FORMAT))
         return context.getCondition();
      else if (dataLabel.equals(TranslatorKeywordManager.XML_COMMENT_FORMAT))
         return context.getNotes();
      else if (dataLabel.equals(TranslatorKeywordManager.XML_SLOT))
         return context.getSlot().getKeyword();
      else if (dataLabel.equals(TranslatorKeywordManager.XML_FORMATTED_VALUE))
         return context.getFormattedValue();

      return "<Simple Fragment was unable to be resolved for data: "
            + dataLabel + " >";
   }

Without going into much detail, we can see a long if/else block (... I know... ) which handles various data request cases. This defines all the possible data one can request from a SimpleFragment, which asks its Context for the resolution.

Here's what a KnowItContext looks like:

public class KnowItContext extends StoryComponentContext {
          .
          . //Constructors and stuff
          .
          .
          StoryComponent
        // The part we actually care about
   /**
    * Get the KnowIt's Binding
    */
   @Override
   public KnowItBinding getBinding() {
      return ((KnowIt) component).getBinding();
   }

   /**
    * Get the KnowIt's Binding's Type
    */
   @Override
   public String getType() {
      final APIDictionaryInterpreter apiDictionary;
      final String bindingType;

      apiDictionary = this.getTranslator().getApiDictionary();
      bindingType = apiDictionary.retrieveTypeCodeSymbol(getBinding()
            .getType());

      if (bindingType != null)
         return bindingType;
      else
         return apiDictionary.retrieveTypeCodeSymbol(((KnowIt) component)
               .getDefaultType());
   }
}

For example, if Language Dictionary says to insert the type of a DoIt's parameter, we create a SimpleFragment with the dataLabel = type in a KnowItContext. From the SimpleFragment we would ask for the type, which knows to ask the KnowItContext for the KnowIt's Binding's type.

Extension & "Unimplemented"

Any request for a value which is undefined in the given Context returns an "Unimplemented" string letting the Language Dictionary author know what they requested is unsupported. However given the structure of the Code Generation system, it should be easy to add new implementations and supported features.

Adding support for new data accesses

When you want to add support for a value in a given XyzContext where XyzContext is a subclass of Context, all you must do is override the request method in XyzContext. As long as the Language Dictionary scopes into the proper Context and you request the dataLabel, it should resolve to the desired value.

Adding additional Contexts

In the case where you want to support additional Contexts, a few things must happen:

  1. Create a subClass of Context, which overrides the desired request methods.
  2. Add this Context to ContextFactory so it can be created
  3. [DEPRECATED]Create a unique Scope (Context) keyword in the apiDictionary.dtd && languageDictionary.dtd and match it to a if/else block in ScopeContext. This will allow for the changing (scoping) of context from the dictionaries.
  4. Make sure to scope into this new context in your Dictionary Authoring before requesting the desired dataLabel.

Name Generation

Code Generation generates names through the CodeGenerationNamifier. The Namifier legalizes the chracters of displayName of StoryComponents and appends integers to the end until they are unique and do not collide with any reserved words in the Language Dictionary. It then stores these in maps so we can reuse the same name when referring to the same component. Extensions can be done to separate unique name generation between functions and variables, as they often can share the same name (which removes unnecessary unification and adds code beautification).

⚠️ **GitHub.com Fallback** ⚠️