Skip to content

MUDScript

Kristian Oye edited this page Dec 9, 2023 · 14 revisions

KMUD is Transpiled to JavaScript (ES6+) script and adds some syntactic sugar to make it more like other OOP languages. This document assumes the reader is familiar with how to write classes in ES6. MUDScript is a "superset" of JavaScript with some notable differences. In many cases, you may opt to change the behaviors through the use of MUDScript compiler options. The default settings are meant to promote strong coding practices and to prevent common mistakes.

  • No late binding

    • MUDScript classes represent well-defined interfaces. All properties and methods must be explicitly declared at compile time.
    • MUDScript class prototypes are sealed when compiling is complete.
    • There is an optional compiler flag called allowLazyBinding that will backfill classes with symbols not explicitly declared in the class body, but the flag is off by default.
    • If you ABSOLUTELY want late binding and you understand the security risks, you may turn off the sealTypesAfterCompile option, but this is HIGHLY discouraged as it allows developers to potentially bypass many of the server security features. Even if the option is turned off, the MUDlib will seal certain types such as the SimulEfun or Master objects and other sensitive types.
  • Multiple inheritance

    • MUDScript classes may be extended by multiple types. Example: class Room extends Container, SmellSupport, LookSupport { ... }
  • Injected superclass

    • All MUDScript classes must extend MUDObject, either directly or through inheritance.
    • If a class does not explicitly extend a type, then the compiler will use the injectedSuperClass option (defaults to 'MUDObject')
  • Custom constructor name

    • MUDScript does not allow the use of the traditional ES6 constructor method as this would prevent multiple inheritance. The compiler may allow the use of the word 'constructor', but it will be substituted for the configured, default constructor name.
    • MUDScript uses a constructor named create unless overridden in the compiler config.
    • MUDScript classes must always call super.create(...) as their first statement
    • Parameters may be passed to create when an instance is created using new (Room, ...args) or await createAsync(Room, ...args
  • Async Constructors

    • MUDScript allows for asynchronous constructors
    • Types with async constructors can only be instantiated in an async context.
  • Custom class modifiers

    • abstract Abstract classes may not be instantiated. They are strictly to be inherited. Any abstract members must be explicitly declared by the first non-abstract child class.
    • final Final classes may not be extended. This may be important for special, privileged types.
    • singleton Singleton classes are limited to a single instance during runtime. The compiler will automatically create an instance when compiling is complete.
    • final and singleton may be combined, but abstract may not.
  • Custom member modifiers:

    • private - Private data is only accessible by the type that defines said method/property
    • protected - Protected data is accessible by the defining type and related types that inherit it.
    • public - Public data is accessible by any other type in the game.
    • package - Package methods and properties may be accessed by types defined in the same module.
    • protected package - Members of such a type are accessible both by children and the module they were defined in (although NOT by other types in the child's module).
    • abstract - Abstract members should have empty bodies and must be implemented by child classes. abstract members can only be defined in abstract classes.
    • final - Final members cannot be redefined in child classes.
    • override - Any time a class defines a member that would mask an inherited member, the override keyword must be used.
  • Custom call operators:

    • -> - Dereferencing call operator. When the LHS (left hand side) of a call may be a wrapper, an object instance, a string variable, or a string literal, you may use the -> call operator (instead of the traditional .). This will ensure that the LHS ALWAYS resolves into an object reference.
    • :: - Scope resolution operator. This allows calling of a specific implementation of a particular member. Example: In the Room example above, the class extends three types (Container, SmellSupport, LookSupport). In order to call the method setItems in SmellSupport, you would call this->SmellSupport::setItems(...) 
    • Override of super behavior. Since MUDScript types may inherit multiple parents, calling parent members using super will call ALL the parent implementations. The return value for such a call is an object in the form of (using Room example) super.getItems() could return { "SmellSupport":{ ... }, "LookSupport":{...} }
  • set/get pseudo operators

    • Classes are not designed to directly hold/store their own data. The game creates an underlying storage object for each object reference.
    • Classes are responsible for performing in-game logic and data validation before passing data to the storage layer using the set/get operators.
    • Setters must call set([value]) to store the value
    • Getters must call get([defaultValue] to retrieve the data. The optional defaultValue is only used if the storage value is undefined.
    • The get() and set() operators may only be used in property definitions.

    Example:

   class Sword {
       set weight(v) {
          if (typeof v === 'number' and v > 0) set(v);
       }

       get weight() { return get(0);  }
  • All async methods MUST be awaited

    • The execution context checks to make sure all async calls are awaited. Attempting to call an async method without awaiting triggers a runtime exception.
  • wrappers (vs objects) - Objects in the game may be represented by direct reference or by using wrappers. A wrapper is a function that maintains a weak reference to an in-game object. It will return the current instance when evaluated. If a piece of code is going to store a reference to a particular object, then it should store a wrapper since the wrapper will update automatically if the underlying object is re-compiled whereas a direct reference will go stale (and prevent proper garbage cleanup). You can think of a wrapper as a pointer.

let player = thisPlayer(),
      playerWrapper = wrapper(player); 

   /* some update changes name from Bob to Frank */

  writeLine(player.name); <-- Prints Bob and causes memory leak
  writeLine(playerWrapper().name); <-- Prints Frank
  • Disallowed ECMA patterns - ECMA script is very flexible, but in order to safeguard against malicious code, certain aspects are considered errors if used in "MUDScript". Use of these patterns would potentially allow code to circumvent the driver's custom call stack maintenance (and thus the security measures). These patterns include (compiler exceptions):
    • func.apply(thisArg, [])
    • func.call(thisArg, ...)
    • func.bind(thisArg)

Attempts to bypass this policy are caught at runtime:

  • func['call'](master(), 'shutdown')
  • func['ca' + 'll'](master(), 'shutdown')
  • var totallySafe = 'call'; func[totallySafe](master(), 'shutdown')
Clone this wiki locally