Skip to content

Under the Hood

MOARdV edited this page Aug 18, 2020 · 5 revisions

Background

RasterPropMonitor has been around since the fall of 2013. I joined the project during the winter of 2013-2014, and I contributed some new variables and functionality off-and-on for a year before taking over primary development in 2015 after Mihara stopped developing KSP mods. Since that time, I've added more features and variables, and I've restructured the code twice in an effort to improve its performance and reduce its garbage production and overhead. There were a few changes that could be done under the hood so that they would impact neither the player nor the IVA creator, but a few required entirely new classes in order to gain their benefits. Which meant, of course that IVA props had to be updated, too.

There was still a need for additional capabilities to be exposed to the IVA creator, but the existing complexity was becoming a serious burden. An example:

I originally added CUSTOM_ variables to allow a creator to make customized boolean variables (variables that are either true or false) by testing each variable to see if it fell within a specified range, and then applying a logical operator (AND, OR, etc) to that value. I later added a MATH_ customized variable that could perform math operations on variables. It was structured in a similar way - each math variable could perform a single type of operation on a list of variables, such as adding them together, or multiplying them together.

There are also SELECT_ variables, which test a sequence of values, and when it finds one that falls within its range will return a separate value (somewhat like a complicated switch statement in a programming language, or a compound if-else block).

There is also an unadvertised feature in RPM that allows events to be triggered based on conditions. This is a very complex feature, and fairly fragile, and I have never been completely happy with it.

Plugin support has always been a bit dicey. Because RPM linked to internal wrapper methods directly, those methods had to follow one of three specific signatures, which made some tasks cumbersome. It introduced limitations in what RPM could do, and there was no clear way around it.

But, back to the MATH_ variables for a moment. If the IVA maker wanted to make a simple equation, like a * b + c (for instance, to convert Celsius to Fahrenheit), he or she would have to use two math variables - one to do the multiplication, and one to do the addition. And there's a lot of code that executes for that small amount of output (two loops plus all of the variable evaluation code, even though two of the three variables involved are constants).

Conditional math was more complex: either a SELECT_ variable had to be used, or a custom variable that could be used to zero out another value using multiplication. The amount of work required, and the disjointed placement of the components of such maths (in multiple variables) created some practical limitations, to say nothing of the computational overhead of all of the evaluations.

What RPM really needed was a way to support scripting - both for evaluating variables on demand, and allowing the creator to make custom compound actions.

Avionics Systems

The problem is that RPM has a lot of baggage attached to it because of its evolution. I don't want to break this old code, since it means any IVA that isn't updated is broken, as well. Adding scripting to RPM would be the easier route, but it'd require interacting with everything that's already a part of RPM, or it would require adding even more classes that are unique to scripting support (while still maintaining the old classes).

Instead, what I chose to do is to start with a clean slate. I've ported a couple of classes from RPM to Avionics Systems to avoid having to rewrite some infrastructure from scratch, while the rest of the design is fresh code.

A new, configuration-incompatible mod will be a nuisance for IVA prop creators (sorry, alexustas), but a clean break MAS to start with a clean slate.

The key change from RasterPropMonitor to Avionics Systems is the introduction of Lua scripting. Where RPM treated variables as hardcoded constant values, MAS uses the value returned from a function. Where RPM actions were limited to a handful of simple actions and an array of plugin actions, MAS allows the use of Lua function calls and basic mathematical and logical expressions.

The first, immediate, benefit of this change is that the IVA creator now has a great deal of flexibility in defining behavior and evaluating numbers.

For a simple example: the retro buttons in the ASET Props pack use a variable that ranges from 0 to 3 to determine the backlight state. In RPM, that is controlled by a MATH_ variable:

RPM_MATH_VARIABLE
{
   name = MyButtonLight
   operator = ADD

   sourceVariable = CUSTOM_PodBacklightOn
   sourceVariable = PERSISTENT_MyButtonOn
   sourceVariable = PERSISTENT_MyButtonOn
}

This causes the PERSISTENT_MyButtonOn variable to be evaluated twice and added together, in addition to CUSTOM_PodBacklightOn being evaluated and added.

In Avionics Systems, this can be simplified to

fc.GetPersistentAsNumber("MyButtonOn") * 2 + PodBacklightOn()

The IVA creator can make the custom function PodBacklightOn, and define it in whatever way makes sense, such as

function PodBacklightOn()
    if fc.GetPower() > 0.0001 and fc.GetPersistentAsNumber("PodBacklight") > 0 then
        return 1
    else
        return 0
    end
end

The larger benefit of using Lua scripting is in actions. Where the JSIActionGroupSwitch had a narrowly-defined set of actions it could take, the COLLIDER_EVENT component of MASComponent is practically open-ended:

Let's say you wanted to add a switch to deploy solar panels, but you wanted to have it have its own safety interlock that disabled the switch while in atmosphere. It's possible in RPM, but it requires setting up a persistent variable that's initialized by a custom variable that returns 1 if you're out of the atmosphere, and 0 otherwise. You would need to repeatedly transfer the custom variable into the persistent variable (which isn't well supported by RPM), or get the user to repeatedly press a button to "update" the safety interlock.

In Avionics Systems, it's much easier. Define a custom Lua function in your configs:

function SafeDeployPanels()

   -- Solar panel state 0 is "retracted"
   if fc.Altitude() > fc.AtmosphereTop() and fc.GetSolarPanelsState() == 0 then
      fc.DeploySolarPanels()
   end

end

and set the onClick field of the COLLIDER_EVENT to SafeDeployPanels().

Another place where MAS is improved is in handling conditional variables. RPM established a convention of 0=false and 1=true (granted, that's actually a common programming convention). However, when testing whether one of these conditional variables is true, RPM required a bounds test with upper and lower parameters - essentially,

if variable > lower_variable and variable < upper_variable then
    Do In-range Action
else
    Do Out-of-range Action
end

What would be better (and what MAS does) is allow simple variables to test if they are greater than zero. One check, and no other variables to evaluate.

A third major improvement is that the monitors (MASMonitor) are extremely flexible. RPM supports one "background handler" per page, where that background handler did one thing - draw orbital parameters, draw line graphs, draw bar graphs, draw a HUD, draw a navball, etc. Text could be drawn over these backgrounds, but it was impossible to combine background handlers.

With MAS, each "background" handler is a node added to the MASMonitor. They can be mixed-and-matched in any order, with any layout. Want a navball, some vertical HUD-style data strips, and a linegraph on the same page? MAS can do that.

Even better, MAS has a "backwards compatibility" mode where it can interact with mods that include RPM background handlers, and it can display those images arbitrarily on-screen (such as a SCANsat display in one quadrant of the screen, with other data rendered in the other three quadrants).

Lua scripting is powerful and flexible, but it comes with a downside. It's an interpreted language running in C#, and it is S-L-O-W, and the MoonSharp interpreter generates a lot of temporary allocations, which lead to garbage collection. As such, I strongly recommend minimizing the use of full-on Lua scripting in variables. For actions (responses to clicks), it's less of an issue, since those do not fire every FixedUpdate.

To help minimize the use of Lua, MAS is able to parse quite a few variables into lambda functions that run natively in C#. This capability is done automatically on any variable found in a config file. For instance, the retro button backlight variable mentioned earlier that called PodBacklightOn() could also be expressed as

variable = fc.GetPersistent("MyButtonOn")*2 + (fc.GetPower() > 0.0001 and fc.GetPersistent("PodBacklight") > 0)

and MAS will generate appropriate lambda functions to evaluate it entirely in C#.

Under the Hood

Or, theory of operation. Or, lots more words.

Two modules are primarily responsible for providing data: the MASFlightComputer and the MASVesselComputer (the data sources). There can be hundreds of components in a complex IVA that want data (the data sinks).

Data Sinks

The props in IVA are the primary data sinks. The majority of them interact with MAS via one or more variables, either directly, as in the case of COLOR_SHIFT, ANIMATION_PLAYER, TEXT_LABEL, etc., or indirectly, such as formatted text output, color variables, etc.

Many of them also use COLLIDER_EVENTs to activate actions in MAS which then have visible effects, such as toggling switches, adjusting knobs, etc.

Due to scripting support in MAS, variables can trigger actions and actions can use variables.

Data Sources

The MASFlightComputer is analogous to the RasterPropMonitorComputer module in RasterPropMonitor: it exists per-part, and all props in a given part route their queries through that flight computer. Data that is unique per-part is tracked by the MASFlightComputer. This includes local crew data and persistent (user-defined) variables.

The MASFlightComputer also manages variable updates. Under the default configuration, every FixedUpdate the MASFlightComputer calls all registered variable scripts to refresh their values. For variables that change, all registered data sinks are notified by a callback system of the new value of the variable.

By using a notification system, Avionics Systems reduces the number of modules polling values in Update or FixedUpdate. It also allows the data sinks to respond immediately to changed variables, where the original RPM JSIVariableAnimator module could conceivably be several frames behind the variable change, which gave the impression of laggy behavior.

MASVesselComputer tracks vessel-wide data. Like MASFlightComputer, it is active every FixedUpdate, during which time it recomputes vessel-specific values (heading, pitch, yaw, and so on). Using a VesselModule allows all vessel-wide parameters to be computed once, instead of once per MASFlightComputer. The MASVesselComputer also manages lists of modules in a vessel, such as ModuleDeployableSolarPanel. These lists are updated when the craft changes. They provide a fast way to track some particular data and specific capabilities in Avionics Systems.

The MASVesselComputer is also responsible for saving and restoring persistent variables inside the save file (persistent.sfs). These variables are collected from all MASFlightComputer nodes in the craft, and saved using the MASFC's unique identifier so that the right values are dispatched to the right flight computers at load time.

From a scripting point of view, there is only the MASFlightComputer, accessed using fc.. It automatically routes vessel-specific queries to the MASVesselComputer.

The data sources are partitioned between MASFlightComputer and MASVesselComputer for two reasons: first, many persistent variables should be unique per pod; making them per-vessel means a switch in one place can affect behavior in other places (which would be weird in cases of docked spacecraft turning on and off parts of other spacecraft). Second, which vessel that a part belongs to can and does change when docking or undocking.

In addition to the two core data sources, there are plugin data sources. These sources are wrappers to code that communicates with other mods, such as getting flaps settings from FAR or parachute status from RealChute.

Scripting

Because the Lua interpreter is incredibly slow in comparison to native C# code, MAS uses a parser on each variable to try to break it down to a simple set of operators, constants, and built-in functions. A "built-in function" is one of the MAS functions (like fc.GetThrottle() or mechjeb.GetDeltaV()).

If MAS can parse the variable without encountering unrecognized tokens, it will create a series of small lambda functions to evaluate the variable, bypassing Lua entirely. This approach is much faster (about 16x or more) than the Lua interpreter during FixedUpdate, and it results in fewer temporary memory allocations, which leads to less pressure on the garbage collector.

Clone this wiki locally