MTropolis/Technical Notes

From ScummVM :: Wiki
< MTropolis
Revision as of 19:20, 7 June 2022 by OneEightHundred (talk | contribs) (Created page with "== Basic paradigm and structure == Much of the information about the mTropolis file format can be found in the notes for the [https://github.com/elasota/MTDisasm MTDisasm disa...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Basic paradigm and structure

Much of the information about the mTropolis file format can be found in the notes for the MTDisasm disassembler tool.

Some of the terminology here might differ from the mTropolis documentation, SDK, or other first-party locations.

A mTropolis project conceptually consists of a few things: An alias palette (a collection of modifier templates that can be inserted in place of aliases elsewhere), a label map (which maps names to IDs of various types), assets, and a tree of structural components and modifiers.

From a user standpoint, the project is broken into sections, which are then broken into subsections, which are broken into scenes. Each subsection typically has one shared scene which remains loaded while other scenes within the subsection are loaded, but there are additional situations where multiple scenes may be loaded at once (intended to be used for popups and such) or even multiple projects (which the engine currently doesn't support).

Each section may be mapped to a segment, and each segment corresponds to one data file. The startup segment contains all headers and mapping information for the rest of the project.

There is no type for a scene - Scenes are actually graphical elements parented under a subsection. The objects for scenes are also always loaded, loading a scene loads objects under the scene object and unloading it removes those objects and removes the scene from the active scene stack, but changes to the scene object itself persist indefinitely.

An important thing to get used to when doing anything with mTropolis games is that there are VERY few object types and the vast majority of interesting things are done via modifiers. Scripts are modifiers. Variables are modifiers. Anything that causes any type of interactivity is a modifier.

Behaviors

Behaviors can be used to group collections of modifiers into a prefab, and they can also be switchable, allowing them to be turned on and off. A disabled behavior modifier will not propagate messages to any of its children.

Variables

Variables are "special" in a few ways: If a variable is aliased, then any aliases referencing that variable actually reference the same instance of that variable. They can also be assigned to read from as if they are values in scripts, although that also has some quirky behavior, especially with object reference variables.

Compound variable modifiers are not actually variable modifiers, they can go in places that require variables but they are actually just containers and they are cloned when aliased.

The alias rule only applies to variables that are directly aliased. So, for example, putting a variable inside of an aliased behavior causes the variable to be cloned for each instance of that behavior.

Message passing and logic

mTropolis has VERY minimal scripting, its scripting language Miniscript has conditions, but no loop support, so complex logic needs to be done via modifiers.

Most of what actually happens is done via messages. Running the engine at debug level 3 and higher will show all message passing and consumption. A message has a target and a simple data payload, and 3 major flags (immediate, cascade, and relay).

The immediate flag causes the message to be sent immediately, and it must be propagated completely before propagation of the previous message continues. In the mTropolis engine, this means it's posted to the VThread (which is basically a suspendable "immediate" task stack), otherwise it's posted to the message queue (which means it will execute after the VThread is exhausted). The "relay" flag causes the message to try firing on every modifier that it propagates to, otherwise message propagation for that object stops when a modifier consumes the message. The "cascade" flag causes the message to propagate to structural children of the object, otherwise it will only target the object itself.

Some message types are designated as "commands." When a command-type message is sent, it is sent as a command directly to the target object and all flags are ignored. However, typically this will result in the object re-propagating it as a message with the same ID to itself and all of its modifiers. So, for example, from a user-facing standpoint, attempting to send a "Play" message to an element causes a "Play" command to be sent to that element, and then the element will post a "Played" message to itself. Internally though, "Play" and "Played" have the same message ID, so sending a message as a command vs. a message must be indicated via a flag on the message dispatch.

GUID resolution and structure loading

Loading of objects in the mTropolis backend is done in multiple stages due to the complex GUID resolution process, inline assets, and other things.

The first step to loading is to load data objects. Data objects are JUST data, but due to weird aspects of the data loading process (like the fact that asset defs can appear anywhere) it's separate.

The second step is conversion of data objects into runtime objects and formation of the scene structure.

The third step is "materialization" which converts loaded objects into a ready-to-use state. This involves replacing any non-variable aliases with a copy of the original, and assigning new GUIDs to the new objects.

Objects are only ever materialized once. Aliasable variables in the global modifier table are materialized when the project is loaded, while everything else is materialized when it's imported into the project. (Important note: This doesn't apply to aliased compound variables because they are not actually considered variable modifiers!)

Objects cloned from already-materialized objects should NOT be materialized again, instead they should be fixed up using an object reference remap table, shallowClone, and visitInternalReferences. Cloning is not currently supported (because Obsidian doesn't use it).

An important aspect of this loading process that GUIDs are resolved in the scope of where they are inserted. This is necessary because mTropolis allows objects to be converted into aliases anywhere that they occur and will NOT do anything to patch up GUID references from where they were, so the GUID needs to be resolvable from the location that the modifier exists.


Scene transitions

mTropolis Player's handling of scene transitions is very buggy/quirky.

Basically, there is a feature called "add to destination scene" (ATDS) which loads the target scene on top of a stack of scenes and adds it to the return list, which is intended for dialogs and such.

Unfortunately, it only works properly in the straightforward case, and transitions are done in a way that basically does the exact actions required for it to work in the typical case but are broken in edge cases.

Basically scenes are ordered in a stack, and there is a scene return list separate from the scene stack. The shared scene is always at the bottom of the stack.

On forward scene transitions:

  • The shared scene is changed to the shared scene of the new scene
  • If not doing an ATDS-type transfer, then the current scene is unloaded
  • If the target scene is not loaded, then it is loaded at the top of the stack

On return transitions:

  • The active scene is unloaded
  • If returning from a non-ATDS transfer, then the return scene is loaded and its hared scene is made active.

This has a bunch of confirmed broken cases:

  • Returning from an ATDS transfer into a different shared scene will not reset the shared scene on return.
  • Transitioning into an ATDS scene that's already in the stack will cause it to be removed on returning. This can even cause no scenes to be loaded.
  • Doing a regular transition out of ATDS stacked scenes and then returning will cause only the top scene to be loaded.
  • Transitioning to the shared scene causes very nonsensical behavior, including doubled-up events, modifiers not working, and other chaos. Currently the mTropolis engine errors out if you attempt this.
  • Probably a bunch of other cases.


Media play times

Sounds, QuickTime movies, and mToons can be commanded to play at any time during initial scene loads, but their play times start when the scene transition completes. This applies even when there is no scene transition because actions that occur prior to the drawing of the first frame may change the media's play state. Obsidian, for instance, starts with a bunch of sounds in a non-paused state and sets them all to paused via a script, so they must not ever play.


Object reference liveness

A lot of things internally go off of an assumption that structural objects and modifiers are NEVER deleted unless the VThread task queue is empty. Basically that means that if any messages are queued or any actions are in the middle of being performed, then objects may not be deleted. The only thing that can delete an object is a scheduled Teardown.

Currently this is unused, but mTropolis supports a "kill" event type which will remove an object when sent to it, and a "kill" attribute that removes an object when it is assigned to.

A big implication of this is that it's okay to use raw pointers to scene objects in VThread tasks. In particular, tasks calling member functions are OK to schedule.


Miniscript

Miniscript has a lot of weird quirks. Most symbols inside of a script are resolved as element members, so "WorldManager" in a script doesn't reference some global object named WorldManager, it compiles into the equivalent of "element.worldmanager" and the actual WorldManager is resolved via hierarchical lookup when attempting to read the attribute from the element.

Also, "this" is not reserved - if you look up the "this" attribute from ANYTHING then it will resolve to the modifier executing the script.


Collision messengers

Collision messengers behave strangely and their exact logic hasn't been determined.

If there are 2 collision detection modifiers on an object and the first one triggers on exit, and the second triggers while in contact, then the second will fire twice.

The "First element only" option behaves kind of nonsensically and contrary to its description. It only prevents multiple collisions from being sent THAT FRAME, but continuously moving the object will cause multiple detections to trigger if they occur on separate frames.

Moving an object from the shared scene will trigger collision with collision messenger modifiers in the main scene, but moving a main scene object will not collide with the same object? Needs more research.

Collisions only occur with visible objects. It seems they are also not capable of colliding with scenes.


mToon event and cel behavior

Setting individual values of mToons has extremely quirky behavior with a lot of unexpected or broken behavior in edge cases. In theory, you can reverse an mToon by setting its range, but what actually happens is that whenever you set a range, the range is sanitized (incorrectly) and the rate is set negative if the unsanitized range was backwards.

Setting the range to a forward or backward range causes the rate to be set negative or positive. In theory this is supposed to ensure that the range is forward and the rate controls play direction, but it actually has some broken cases.

Some sample situations, assuming an mToon with 7 valid frames:

 set mtoon.range to (20 thru 10)

... will result in a negative rate and range of 10 thru 7.

 set mtoon.range.start to 2
 set mtoon.range.end to 4
 set mtoon.range.start to 5
 set mtoon.range.end to 6

... will result in a positive rate and a range of 4 thru 6, because the range becomes "5 thru 4" on the third line, which inverts to 5 thru 4.

The cel is always set to a valid value, but this is subject to some quirks as well: The cel can be set to an out-of-range value and will go to that cel. (The Obsidian booth hint room depends on this behavior.)

Cel changes from scripts and clamping do not fire events. "At first cel" is only fired from play control looping or starting the animation. "At last cel" is only fired from play control. If the animation is 1 frame, only "at first cel" is fired.

"At first cel" is also fired on auto-play even if the animation is paused.

The inter-frame timer is not changed by changing the cel from scripts.