Skip to content

Interface for other KSP mods

Robin Leroy edited this page May 20, 2020 · 37 revisions

Principia provides an API that allows other mods to take its effects into account.

Mods should use the interface by reflection.

The interface is provided by an assembly whose AssemblyName.Name is principia.ksp_plugin_adapter (the actual file name is different, currently ksp_plugin_adapter.dll in the Principia directory; callers should not depend on the file name).

The ExternalInterface object

The interface consists of instance methods of the class principia.ksp_plugin_adapter.ExternalInterface.

ExternalInterface.Get()

An instance of this class can be obtained by calling the static method

principia.ksp_plugin_adapter.ExternalInterface.Get()

which:

  • returns an object of type ExternalInterface if the Principia ScenarioModule and the Principia native DLLs are loaded;
  • returns null if the Principia ScenarioModule is not loaded, for instance in the editor scene, where Principia does not run;
  • throws DllNotFoundException if the Principia native DLL could not be loaded; this indicates an incorrect Principia installation.

Interface types

The interface types are declared in namespace principia.ksp_plugin_adapter.

☡ We give sample declarations with fields below, however, it is unspecified whether the members are fields or properties; interfacing mods should accept either by reflection. Not accepting both will cause future breakage in the event we need to change between fields and properties. Accepting both will keep calling code operational when these refactorings take place.

It is also unspecified whether the interface types are value types or reference types (struct or class). If they are reference types, they have a default constructor. Since the types are boxed when accessed by reflection, this mostly does not matter. When constructing an object of an interface type to pass it as a parameter to an interface function, callers should use Activator.CreateInstance(Type).

XY

public struct XY {
  public double x;
  public double y;
}

XYZ

public struct XYZ {
  public double x;
  public double y;
  public double z;
}

Interface functions

The example usages of the interface functions given in this section make use of some utilities (classes Principia and Reflection) to limit the reflection boilerplate at the call site. We have tried to make the utility identifiers self-explanatory; see the appendix for their definition.

The types of the exceptions thrown by erroneous interface calls are unspecified. When an exception is thrown, additional information may be found in the Principia logs.

CelestialGetPosition

  public XYZ CelestialGetPosition(int body_index, double time);

Returns the position of the body whose flightGlobalsIndex is body_index at the given time.

Units and reference systems

The parameter time is in KSP’s universal time, in seconds, as given, e.g., by Planetarium.GetUniversalTime().

The result is given in metres, in a reference frame whose origin is the barycentre of the solar system, and whose axes are those of Unity’s world.

CelestialGetSurfacePosition

    public XYZ CelestialGetSurfacePosition(
      int body_index,
      double planetocentric_latitude_in_degrees,
      double planetocentric_longitude_in_degrees,
      double radius,
      double time);

Returns the position of the point at the given planetocentric coordinates and at a distance of radius from the body centre on the body whose flightGlobalsIndex is body_index at the given time.

Units and reference systems

The angles planetocentric_latitude_in_degrees and planetocentric_longitude_in_degrees are, as their name indicates, in degrees. These are planetocentric coordinates, rather than planetographic coordinates: they do not take into account any reference ellipsoid, and are straightforward spherical coordinates, with positive longitudes eastwards.

The radius is in metres.

The parameter time is in KSP’s universal time, in seconds, as given, e.g., by Planetarium.GetUniversalTime().

The result is given in metres, in a reference frame whose origin is the centre of the body, and whose axes are those of Unity’s world.

GeopotentialGetCoefficient

  public XY GeopotentialGetCoefficient(int body_index,
                                       int degree,
                                       int order);

Returns the normalized geopotential coefficient of the given degree and order of the body whose flightGlobalsIndex is body_index. For degree 𝑛 and order 𝑚, the x member of the result is 𝐶𝑛𝑚 and the y member is 𝑆𝑛𝑚.

Throws an exception if:

  • the Principia plugin is not started;
  • there is no CelestialBody whose flightGlobalsIndex is body_index;
  • the relation 0orderdegree is not satisfied.

Notes

The coefficients 𝐶𝑛𝑚 and 𝑆𝑛𝑚 may be given as normalized or unnormalized coefficients (most often the former). See the IERS conventions (2010), chapter 6 for definition of the normalized and unnormalized coefficients. While the IERS conventions use an overline to denote normalization, the normalized coefficients are often referred to as 𝐶𝑛𝑚 as well.

Callers should check the convention for their usage, and unnormalize the result of GeopotentialGetCoefficient as needed.

For Earth, the normalized value of 𝐶32 is about 9.0476×10−7, while the unnormalized value is 3.0904×10−7.

The zonal harmonics 𝐶𝑛0 are often specified using 𝐽𝑛. 𝐽𝑛 is always given unnormalized, 𝐽𝑛 = −𝐶𝑛0 with the unnormalized value of 𝐶𝑛0.

With the normalized value of 𝐶𝑛0, this becomes 𝐽𝑛 = −𝐶𝑛0 √(2𝑛 + 1).

Example: computing 𝐽2 for Earth

var principia = Principia.Get();
CelestialBody earth = FlightGlobals.GetHomeBody();
var c20_s20 = Reflection.Call(principia, "GeopotentialGetCoefficient")(
    earth.flightGlobalsIndex, 2, 0);
double c20 = Reflection.GetFieldOrPropertyValue<double>(c20_s20, "x");
double j2 = -c20 * Math.Sqrt(5);

GeopotentialGetReferenceRadius

  public XY GeopotentialGetReferenceRadius(int body_index);

Returns the value in metres of the reference radius of the geopotential model for the body whose flightGlobalsIndex is body_index.

Throws an exception if:

  • the Principia plugin is not started;
  • there is no CelestialBody whose flightGlobalsIndex is body_index.

Notes

The reference radius is the quantity denoted by 𝑎𝑒 in the IERS conventions (2010), chapter 6.

Example: computing the precession of the ascending node due to 𝐽2 (in radians per second)

double J2NodalPrecession(Orbit orbit) {
  var principia = Principia.Get();
  var c20_s20 = Reflection.Call(principia, "GeopotentialGetCoefficient")(
      orbit.referenceBody.flightGlobalsIndex, 2, 0);
  double c20 = Reflection.GetFieldOrPropertyValue<double>(c20_s20, "x");
  double j2 = -c20 * Math.Sqrt(5);
  double ae =
      Reflection.Call<double>(principia, "GeopotentialGetReferenceRadius")(
          orbit.referenceBody.flightGlobalsIndex);

  double i = orbit.inclination * Math.PI / 180;
  double n = orbit.meanMotion;
  double p = orbit.semiLatusRectum;
  return -3.0 / 2.0 * n * j2 * Math.Pow(ae / p, 2) * Math.Cos(i);
}

VesselGetPosition

  public XYZ VesselGetPosition(string vessel_guid, double time);

Returns the position of the vessel whose Vessel.id.ToString() is vessel_guid, at the given time.

Units and reference systems

The parameter time is in KSP’s universal time, in seconds, as given, e.g., by Planetarium.GetUniversalTime().

The result is given in metres, in a reference frame whose origin is the barycentre of the solar system, and whose axes are those of Unity’s world.

Appendix: reflection utilities

// Principia-specific utilities.
public static class Principia {
  public static string AssemblyName() {
    foreach (var loaded_assembly in AssemblyLoader.loadedAssemblies) {
      if (loaded_assembly.assembly.GetName().Name == "principia.ksp_plugin_adapter") {
        return loaded_assembly.assembly.FullName;
      }
    }
    throw new DllNotFoundException(
        "principia.ksp_plugin_adapter not in AssemblyLoader.loadedAssemblies");
  }

  public static Type GetType(string name) {
    return Type.GetType(
      $"principia.ksp_plugin_adapter.{name}, {AssemblyName()}");
  }

  // principia.ksp_plugin_adapter.ExternalInterface.Get().
  public static object Get() {
    return GetType("ExternalInterface")
        .GetMethod("Get")
        .Invoke(null, null);
  }
}

// This class provides the following methods:
// — Reflection.Call(obj, "name")(args);
// — Reflection.GetFieldOrPropertyValue(obj, "name");
// — Reflection.SetFieldOrPropertyValue(obj, "name", value).
// The following generics are equivalent to casting the result of the
// non-generic versions, with better error messages:
// — Reflection.Call<T>(obj, "name")(args) for (T)Reflection.Call(obj, "name")(args);
// — Reflection.GetFieldOrPropertyValue<T>(obj, "name") for
//   (T)Reflection.GetFieldOrPropertyValue(obj, "name").
public static class Reflection {
  // Returns the value of the property or field of |obj| with the given name.
  public static T GetFieldOrPropertyValue<T>(object obj, string name) {
    if (obj == null) {
      throw new NullReferenceException(
          $"Cannot access {typeof(T).FullName} {name} on null object");
    }
    Type type = obj.GetType();
    object result = null;
    FieldInfo field = type.GetField(name, public_instance);
    PropertyInfo property = type.GetProperty(name, public_instance);
    if (field != null) {
      result = field.GetValue(obj);
    } else if (property != null) {
      result = property.GetValue(obj, index : null);
    } else {
      throw new MissingMemberException(
          $"No public instance field or property {name} in {type.FullName}");
    }
    try {
      return (T)result;
    } catch (Exception exception) {
      throw new InvalidCastException(
          $@"Could not convert the value of {
              (field == null ? "property" : "field")} {
              (field?.FieldType ?? property.PropertyType).FullName} {
              type.FullName}.{name}, {result}, to {typeof(T).FullName}",
          exception);
    }
  }

  public static void SetFieldOrPropertyValue<T>(object obj, string name, T value) {
    if (obj == null) {
      throw new NullReferenceException(
          $"Cannot set {typeof(T).FullName} {name} on null object");
    }
    Type type = obj.GetType();
    FieldInfo field = type.GetField(name, public_instance);
    PropertyInfo property = type.GetProperty(name, public_instance);
    if (field == null && property == null) {
      throw new MissingMemberException(
          $"No public instance field or property {name} in {type.FullName}");
    }
    try {
      field?.SetValue(obj, value);
      property?.SetValue(obj, value, index : null);
    } catch (Exception exception) {
      throw new ArgumentException(
          $@"Could not set {
              (field == null ? "property" : "field")} {
              (field?.FieldType ?? property.PropertyType).FullName} {
              type.FullName}.{name} to {typeof(T).FullName} {
              value?.GetType().FullName ?? "null"} {value}",
          exception);
    }
  }

  public static object GetFieldOrPropertyValue(object obj, string name) {
    return GetFieldOrPropertyValue<object>(obj, name);
  }

  public delegate T BoundMethod<T>(params object[] args);

  public static BoundMethod<T> Call<T>(object obj, string name) {
    if (obj == null) {
      throw new NullReferenceException($"Cannot call {name} on null object");
    }
    Type type = obj.GetType();
    MethodInfo method = type.GetMethod(name, public_instance);
    if (method == null) {
     throw new KeyNotFoundException(
         $"No public instance method {name} in {type.FullName}");
    }
    return args => {
      object result = method.Invoke(obj, args);
      try {
        return (T)result;
      } catch (Exception exception) {
        throw new InvalidCastException(
            $@"Could not convert the result of {
                method.ReturnType.FullName} {
                type.FullName}.{name}(), {result}, to {typeof(T).FullName}",
            exception);
      }
    };
  }

  public static BoundMethod<object> Call(object obj, string name) {
    return Call<object>(obj, name);
  }

  private const BindingFlags public_instance =
      BindingFlags.Public | BindingFlags.Instance;
}
Clone this wiki locally