Results 1 to 5 of 5

Thread: Plugin Posting Rules And Official Documentation

  1. #1
    Junior Rocket Scientist N3X15's Avatar
    Join Date
    Dec 2011
    Posts
    605
    Blog Entries
    17

    Plugin Posting Rules And Official Documentation

    As many of you know, KSP 0.14 added a new system to enable creating new kinds of parts. For an example, you can make weapons, lights, 'talk' to other parts, etc. Anything you can conceive of that can go into a part can become a part module.

    However, before we get started, some general guidelines.

    Rules

    • Because of the nature of these things, All plugin sourcecode must be made publically available, either by including it in your parts zip, or by posting it on a public code repository (like github). This will help prevent malware.
    • If you contact a website or other network or computer system with your plugin, you must tell users exactly what you\'re sending or receiving in a clear and obvious way. This means no fine text and no big words.
    • Your plugin must not edit, delete, or create files outside of the KSP installation folder. This will, again, help protect user privacy and avoid destructive behavior.
    • Do not pop up ads or install additional software.
    • Do not decompile, modify or distribute any of the DLLs or other files KSP comes with beyond stuff in the Parts folder. Follow the EULA. For assemblies, you may only use exposed public or protected members of classes, and you may not examine the code within any member.
    • You are not allowed to remove or modify Squad logos or copyright notices.
    • Keep it clean. No cursing, ....ography, or offensive material here, please.
    • Get permission before using or editing someone else\'s work, unless their license explicitly gives you open permission. This includes external libraries.


    For those of you still confused, here is a simplified policy regarding assembly browsers:

    This (the signature for public and protected members) is okay to look at and discuss:
    Code:
    public int LolAMethodThatDoesThings(Herp derp);
    This (the code inside the member, PLUS members that are private) is NOT to be looked at, discussed, or distributed in any form or fashion:
    Code:
    public int LolAMethodThatDoesThings(Herp derp) { return derp.asNumber/0; }
    Subject Guidelines

    In your subject, you must include the following:
    • [PLUGIN] at the beginning (You can combine with other tags, like PARTS or whatever: [PLUGIN, PARTS])
    • What version KSP it runs on
    • What state your plugin is in (Alpha, Beta, Development, Release)



    Topic Contents

    Include the following in your topic.

    • Plugin name and version
    • Author(s)
    • License (GPL, BSD, CC, other licenses that allow showing sourcecode)
    • Pictures, if possible


    If you find someone breaking these rules, report the post and we\'ll take a look at it and take appropriate action. If you have questions, ask here.

    Tutorials

    I\'m starting to put up documentation on the temporary wiki (since I can\'t edit the official one). It's an ongoing project, but the basics are there. Squad will be documenting the more advanced things in the API in the coming weeks, so explore what you can see and play with it.


    BY THE WAY
    I was under the erroneous impression that the Visual Studio Object Browser exposes private members and cracked down on it. I was wrong, and if I gave you crap about it, I sincerely apologize. I use a software add-in that DOES expose private members for use with some of my non-KSP projects, and got it mixed up with the object browser. Therefore, my bad, and I apologize.
    Last edited by Kaufman; 4th August 2012 at 05:31.

  2. #2

    0.15 code update - PartModule, KSPField, KSPEvent, ConfigNode and PartResource

    Welcome to the 0.15 patch. I want to outline a number of new bits that have come in and show you how to use them. These bits have filtered down from the new code which has been written to be easier to work with and easier to expand.

    Please note that modding functionality has not changed at all. You can continue to use whatever interface you were doing and everything currently should be backwards compatible. However we are moving away from functional code in Part for reasons outlined below.



    What and why

    Firstly however I want to talk about the reasons for the change so you can understand (and comment on) the direction moving to 0.16 and beyond.

    Originally KSP was designed as a single-vessel orbital physics game. As the success of the project increased people obviously wanted more and so more features were added into the code. Multiple vessels, etc. The community plugin development also wasnt originally planned and was tacked on at some point. All these factors combined means that the core code is not easy for us to expand upon or easy for you lot to write plugins for. No doubt we all have big plans for KSP and to implement them properly we need to expand the base and make it fit for purpose.


    So the plan has been to abstract what a Part and a Vessel actually are. A vessel is a collection of connected parts, but with some orbit stuff tacked on. A part is a physical object which can be connected to another, also has some code tacked on too.

    Therefore..

    - Part gets split into Part (model & physical connection) and PartModule (functional code).
    - Vessel gets split into PartAssembly (a list of Parts) and Vessel (some orbit stuff).

    Having PartAssembly seperate from Vessel means that we can create other types of groups of parts. Internal spaces, virtual cockpits, kerbal personalities, buildings, etc. Thus one editor screen can function as an editor for all types of assembly.

    Having PartModule seperate from Part means we get to a smaller group of core parts which just define types of attachment logic and they can be have many (or none) code behaviours layered onto it.


    'All very well and good', you say, 'but its a complicated horrible mess of interconnectedness how are we gonna deal with that? Eh? Eh?!'. A fine question, ignorance is bliss, data and structure should be on a need to know basis. Having to learn how a specific thing works is a chore.

    So to deal with this is an in built event messaging system. It deals, from your point of view, in strings. (It doesnt ofc, it uses pre-reflection and ints for lil extra performance). Basically a PartModule, Part or Vessel can send messages to things and recieve events from things. A code module ideally should be coded that it is ignorant of anything outside it. There are cases where you may want a group of modules to communicate with eachother, these can either be done with the messaging or ofc through direct references as before.

    Defining attributes in your module code of KSPEvent (on methods) or KSPField (on fields) exposes that event/field to the internal reflection. These attributes also contain data for linking the event/field to the part action gui. You can also alter the values for your event and field attributes at runtime to control the flow of your code.

    That last bit sounds confusing. When you see it in action later it\'ll sink in.


    Lastly we need a simple, easy and powerful way of defining configuration files. For this purpose ConfigNode was born. Its an incredibly simple recursive node/value list and can be used a few ways. It contains all the code for reading and writing all the config files for the game.



    PartModule

    So, the long awaited PartModule class.

    This is a piece of code which can be attached to any Part in its config file. You can add as many as you like and add multiple of the same type should you need to. They are added to the Part GameObject itself and you still have access to part and vessel directly from PartModule.

    PartModules are very simple and currently contain only 6 overrides (compared to Part\'s.. err.. 34). Here is a PartModule showing all of its overrides in place...
    [spoiler]
    Code:
    public class ModuleTest : PartModule
    {
      /// <summary>
      /// Constructor style setup.
      /// Called in the Part\'s Awake method. 
      /// The model may not be built by this point.
      /// </summary>
      public override void OnAwake()
      {
      }
    
      /// <summary>
      /// Called during the Part startup.
      /// StartState gives flag values of initial state
      /// </summary>
      public override void OnStart(StartState state)
      {
      }
    
      /// <summary>
      /// Per-frame update
      /// Called ONLY when Part is ACTIVE!
      /// </summary>
      public override void OnUpdate()
      {
      }
    
      /// <summary>
      /// Per-physx-frame update
      /// Called ONLY when Part is ACTIVE!
      /// </summary>
      public override void OnFixedUpdate()
      {
      }
    
      /// <summary>
      /// Called when PartModule is asked to save its values.
      /// Can save additional data here.
      /// </summary>
      /// <param name='node'>The node to save in to</param>
      public override void OnSave(ConfigNode node)
      {
      }
    
      /// <summary>
      /// Called when PartModule is asked to load its values.
      /// Can load additional data here.
      /// </summary>
      /// <param name='node'>The node to load from</param>
      public override void OnLoad(ConfigNode node)
      {
      }
    }
    [/spoiler]

    Looks rather simple doesnt it and thats because it is. Its not set in stone and if we need more we can add them.
    In reality I hope you only have to use very few of those overrides for any given module. Most of the loading and saving will be taken care of KSPField attributes unless you want to save complicated stuff.

    You can use any Unity MonoBehaviour method apart from Awake. You can use OnDestroy as a destructor. Update and FixedUpdate are perfectly fine (you should check if the part is controllable first). There is no guarantee of having a rigidbody attached in the standard FixedUpdate.


    Example #1 - KSPEvents and KSPFields

    So lets look a more complicated example with communication between two modules and some KSPEvent/KSPField malarky.

    Here is ModuleCommand. It would sit on a command pod and scream orders to everything else. In this case it just screams one order, InputGearToggle.
    [spoiler]
    Code:
    public class ModuleCommand : PartModule
    {
      public override void OnUpdate()
      {
        if (FlightInputHandler.state.gearDown || FlightInputHandler.state.gearUp)
        {
          part.SendEvent('InputGearToggle');
        }
      }
    }
    [/spoiler]

    The key feature of ModuleCommand is part.SendEvent(evtName) this tells its host part to send an event into the assembly. The Part sends the event to all of its modules and all of its attached neighbours, who in turn send it to their modules, ad infinitum.


    So we need something to respond to that order. Here is ModuleAnimatorLandingGear. Technically it doesnt animate anything. It just changes a float from 0 to 1 and renames its gui event to reflect its current state.
    [spoiler]
    Code:
    public class ModuleAnimatorLandingGear : PartModule
    {
      [KSPField]
      public float gearExtension = 0f;
    
      [KSPEvent(guiActive = true, guiName = 'Toggle Gear')]
      public void InputGearToggle()
      {
        if (gearExtension == 0f)
        {
          gearExtension = 1f;
    
          Events['InputGearToggle'].guiName = 'Retract gear';
        }
        else
        {
          gearExtension = 0f;
    
          Events['InputGearToggle'].guiName = 'Extend gear';
        }
      }
    
      public override void OnLoad(ConfigNode node)
      {
        if (gearExtension == 0f)
        {
          Events['InputGearToggle'].guiName = 'Extend gear';
        }
        else
        {
          Events['InputGearToggle'].guiName = 'Retract gear';
        }
      }
    }
    [/spoiler]

    Plenty new going on here!
    We have one field, gearExtension which has the KSPField attribute applied. This makes this field persistant and it will be written to/from any saves as required.
    After KSPFields are parsed by PartModule then the OnLoad method is fired. As we have nothing else to load, we use OnLoad as a method for working out what to do with our data. In this instance that means setting the event gui name to be correct.

    The limitation for adding the KSPField attribute is that it can only be applied to classes which implement the IConfigNode interface (more on this later) or one of the types; string, bool, int, float, Vector2, Vector3, Vector4 or Quaternion
    Here KSPField attribute from the game source.. [spoiler]
    Code:
    /// <summary>
    /// Attribute applied to fields to make them persistant or available to the part action GUI
    /// 
    /// Automatic persistance can only be applied to types which implement the IConfigNode interface or
    /// one of the following..
    /// string, bool, int, float, Vector2, Vector3, Vector4 or Quaternion
    /// </summary>
    [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple = false)]
    public class KSPField : System.Attribute
    {
      /// <summary>
      /// Is this field persistant?
      /// </summary>
      public bool isPersistant;
    
      /// <summary>
      /// Is this field active on gui
      /// </summary>
      public bool guiActive;
    
      /// <summary>
      /// Is this field active on gui
      /// </summary>
      public string guiName;
    
      /// <summary>
      /// Is this field active on gui
      /// </summary>
      public string guiUnits;
    
      /// <summary>
      /// The gui format string for this field (D4, F2, N0, etc). Blank if none
      /// </summary>
      public string guiFormat;
    
      /// <summary>
      /// string category id
      /// </summary>
      public string category;
    
      public KSPField()
      {
        this.isPersistant = true;
        this.guiName = '';
        this.guiUnits = '';
        this.guiFormat = '';
        this.category = '';
      }
    }
    [/spoiler]

    The method InputGearToggle has the KSPEvent attribute applied. This makes this event able to be internally reflected and recieve events thus is the entry point for most functionality. It will be fired in response to the ModuleCommand\'s part.SendEvent. KSPEvent also makes this method available to the gui in form of a labelled button. You can change the guiActive, guiName or any other KSPEvent value at run time by using the Events list.
    Here is the KSPEvent attribute from the game source..
    [spoiler]
    Code:
    /// <summary>
    /// Tells the compiler that this method is an action and allows you to set up
    /// the KSP specific stuff. 
    /// ** REQUIRED BY ALL ACTION METHODS **
    /// </summary>
    [System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = true)]
    public class KSPEvent : System.Attribute
    {
      /// <summary>
      /// The external name of this action
      /// </summary>
      public string name;
    
      /// <summary>
      /// Is this action assigned as the part\'s default? 
      /// * Will override any previous default *
      /// </summary>
      public bool isDefault;
    
      /// <summary>
      /// Is this action initially active?
      /// </summary>
      public bool active;
    
      /// <summary>
      /// Is this action available to the user?
      /// </summary>
      public bool guiActive;
    
      /// <summary>
      /// The guiIcon name (guiAction must be true)
      /// </summary>
      public string guiIcon;
    
      /// <summary>
      /// The gui name for this action (userAction must be true)
      /// </summary>
      public string guiName;
    
      /// <summary>
      /// A string category id so can display all actions of certain types
      /// </summary>
      public string category;
    
    
      public KSPEvent()
      {
        this.name = '';
    
        this.isDefault = false;
        this.active = true;
        this.allowStaging = false;
        this.autoStaging = false;
        this.guiActive = false;
        this.guiIcon = '';
        this.guiName = '';
        this.category = '';
      }
    }
    [/spoiler]



    Si non confectus, non reficiat.

  3. #3

    Re: 0.15 code update - PartModule, KSPField, KSPEvent and ConfigNode


    Example #2 - IConfigNode and ConfigNode

    Ok so time for something more meaty. Here is my take on an aerodynamic lift module. It uses a class called FloatCurve to create the lift/drag vs angle of attack graphs for standard aerofoils. FloatCurve also implements IConfigNode so it can be used with KSPField.
    [spoiler]
    Code:
    public class ModuleAerodynamicLift : PartModule
    {
      /// <summary>
      /// Planform area of lifting surface in m^2
      /// </summary>
      [KSPField]
      public float planformArea = 10f;
    
    
      /// <summary>
      /// Overall lift factor for this wing
      /// </summary>
      [KSPField]
      public float liftFactor = 1f;
    
      /// <summary>
      /// Overall drag factor for this wing
      /// </summary>
      [KSPField]
      public float dragFactor = 1f;
    
    
      /// <summary>
      /// FloatCurve of lift vs angle of attack. Angle is abs cosine of angle (0 -> 1)
      /// </summary>
      [KSPField]
      public FloatCurve liftAoA;
    
    
      /// <summary>
      /// FloatCurve of drag vs angle of attack. Angle is abs cosine of angle (0 -> 1)
      /// </summary>
      [KSPField]
      public FloatCurve dragAoA;
    
    
      /// <summary>
      /// Model transform name for center of lift
      /// </summary>
      [KSPField]
      public string centerOfLiftTransformName;
    
    
      /// <summary>
      /// grabbed OnStart from the model transform named liftTransformName
      /// </summary>
      public Transform centerOfLiftTransform;
    
    
      /// <summary>
      /// Sets up the float curves if they\'re not already set up
      /// </summary>
      public override void OnAwake()
      {
        if (liftAoA == null)
          liftAoA = new FloatCurve();
        if (dragAoA == null)
          dragAoA = new FloatCurve();
      }
    
      /// <summary>
      /// Grabs center of lift transform from model
      /// </summary>
      /// <param name='state'></param>
      public override void OnStart(StartState state)
      {
        if (centerOfLiftTransform == null)
        {
          centerOfLiftTransform = part.FindModelTransform(centerOfLiftTransformName);
    
          if (centerOfLiftTransform == null)
            Debug.LogError('ModuleAerodynamicLift: liftTransform is null!');
        }
      }
    
      /// <summary>
      /// Calculates and applied the lift/drag force from the aerofoil
      /// </summary>
      public override void OnFixedUpdate()
      {
        if (centerOfLiftTransform == null)
          return;
    
        Vector3 force = CalculateForce();
    
        part.Rigidbody.AddForceAtPosition(force, centerOfLiftTransform.position, ForceMode.Force);
      }
    
      /// <summary>
      /// Calculates lift/drag according the simple aerofoil equations...
      /// Lift: L = (CL)(1/2)(dens)(V^2)(area)  or  L = (CL)(q)(S)    q(dyn pressure) = (1/2)(dens)(V^2)
      /// Drag: D = (CD)(1/2)(dens)(V^2)(area)  or  D = (CD)(q)(S)
      /// </summary>
      /// <returns>Overall force vector</returns>
      private Vector3 CalculateForce()
      {
        // grab world point and relative velocity
        Vector3 worldVelocity = part.Rigidbody.GetPointVelocity(centerOfLiftTransform.position);
    
        // note we use centerOfLiftTransfrom from the model to calculate relative. This will take into account any part mirroring 
        Vector3 relativeVelocity = centerOfLiftTransform.InverseTransformDirection(worldVelocity);
        Vector3 velocityNorm = relativeVelocity.normalized;
    
    
        // only need the speed squared - saves us a square root
        float speedSqr = relativeVelocity.sqrMagnitude;
    
    
        // calc the angle of attack
        float vDot = Vector3.Dot(velocityNorm, centerOfLiftTransform.up.normalized);
        float vDotNorm = (vDot + 1f) * 0.5f;
    
        float absVDot = Mathf.Abs(vDot);
        float abs1MVDot = 1f - absVDot;
    
    
        // dynamic pressure 
        float dynPressure = 0.5f * (float)vessel.atmDensity * speedSqr;
    
        // calc coefficient of lift and drag from the factors and the float curves
        float cL = liftFactor * liftAoA.Evaluate(abs1MVDot);
        float cD = dragFactor * dragAoA.Evaluate(abs1MVDot);
    
        // calc lift, drag and add to get overall
        Vector3 lift = centerOfLiftTransform.up * (cL * dynPressure * planformArea);
        Vector3 drag = -(worldVelocity.normalized) * (cD * dynPressure * planformArea);
        Vector3 force = lift + drag;
    
    
        // some debug stuff
        string str = '';
        str += 'AoA: ' + abs1MVDot;
        str += ' cL: ' + cL;
        str += ' cD: ' + cD;
        Debug.Log(str);
    
        Debug.DrawLine(centerOfLiftTransform.position, centerOfLiftTransform.position + lift * 100f, Color.green);
        Debug.DrawLine(centerOfLiftTransform.position, centerOfLiftTransform.position + drag * 100f, Color.cyan);
        Debug.DrawLine(centerOfLiftTransform.position, centerOfLiftTransform.position + worldVelocity * 100f, Color.magenta);
    
    
        // et voila
        return force;
      }
    }
    [/spoiler]

    Really its a very simple module. All of its persistance is handled by its KSPFields and it basically adds lift/drag based on standard aerofoil model.
    The FloatCurve class is a Unity AnimationCurve wrapped up in an IConfigNode extending interface. Remember in order to use KSPField on a class successfully it needs to implement IConfigNode.

    Here is the IConfigNode interface.
    [spoiler]
    Code:
    /// <summary>
    /// Can this item be saved using a KSPField persitance object. KSPField creates a subnode for this type
    /// </summary>
    public interface IConfigNode
    {
      void Load(ConfigNode node);
    
      void Save(ConfigNode node);
    }
    [/spoiler]
    It requires two methods, Load(ConfigNode) and Save(ConfigNode). The implementing class must be able to be instantiated blind and then have OnLoad called to load its values in.

    ConfigNode consists of a recursive node/value list.
    In essence it looks like this..
    [spoiler]
    Code:
    public class ConfigNode
    {
      public string name;
      public List<ConfigNode> nodes;
      public List<ConfigNode.Value> values;
    
      public class Value
      {
       public string name;
       public string value;
      }
    }
    [/spoiler]

    For loading use; HasValue, GetValue, GetValues, HasNode, GetNode & GetNodes
    For saving use; AddValue or AddNode
    Every value you add is a string and should be parsable to/from what you set it to.


    Now lets have a look in FloatCurve to see that in action..
    [spoiler]
    Code:
    [System.Serializable]
    public class FloatCurve : IConfigNode
    {
      [SerializeField]
      private AnimationCurve fCurve;
    
      public float minTime { get; private set; }
      public float maxTime { get; private set; }
    
      public FloatCurve()
      {
        fCurve = new AnimationCurve();
    
        minTime = float.MaxValue;
        maxTime = float.MinValue;
      }
    
      public void Add(float time, float value)
      {
        fCurve.AddKey(time, value);
    
        minTime = Mathf.Min(minTime, time);
        maxTime = Mathf.Max(maxTime, time);
      }
    
      public float Evaluate(float time)
      {
        return fCurve.Evaluate(time);
      }
    
      private static char[] delimiters = new char[] { \' \', \',\', \';\', \'\t\' };
    
      public void Load(ConfigNode node)
      {
        string[] values = node.GetValues('key');
    
        int vCount = values.Length;
    
        string[] valueSplit;
        for (int i = 0; i < vCount; i++)
        {
          valueSplit = values[i].Split(delimiters, System.StringSplitOptions.RemoveEmptyEntries);
    
          if (valueSplit.Length < 2)
          {
            Debug.LogError('FloatCurve: Invalid line. Requires two values, \'time\' and \'value\'');
          }
    
          Add(float.Parse(valueSplit[0]), float.Parse(valueSplit[1]));
        }
      }
    
      public void Save(ConfigNode node)
      {
        for (int i = 0; i < fCurve.keys.Length; i++)
        {
          node.AddValue('key', fCurve.keys[i].time + ' ' + fCurve.keys[i].value);
        }
      }
    }
    [/spoiler]

    As you can see it has a list of values called \'key\' and each value is made up of a space seperated time/value pair. I\'ve added extra delimiters into the code cuz you/I might forget and comma seperate them or something.



    Example #3 - part.cfg

    So now we get to the point of adding these things into a part\'s config file. You should know that every config file is parsed by ConfigNode now so can be considered in the node/value list paradigm.

    Here is ModuleAerodynamicLift module from example #2 added to the end of DeltaWing\'s part.cfg
    [spoiler]
    Code:
    MODULE
    {
     name = ModuleAerodynamicLift
     liftFactor = 0.001
     dragFactor = 0.001
     liftTransformName = CenterOfLift
    
     liftAoA
     {
       key = 0.0 1
       key = 0.2 3
       key = 0.4 4
       key = 0.6 1
       key = 0.7 0
       key = 1.0 -1
     }
     dragAoA
     {
       key = 0.0 1
       key = 0.2 3
       key = 0.5 5
       key = 0.7 6
       key = 1.0 7
     }
    }
    [/spoiler]

    MODULE is a subnode of Part and you can have as many as you like.

    This particular module relys on you having the transform named 'CenterOfLift' somewhere in the DeltaWing heirarchy, which it may not when we go to press. Most of the KSPField values are in the usual style of valueName=value however you can see how the two IConfigNode implementing FloatCurves are represented as subnodes of the module node. KSPField will make any IConfigNode implementing class a subnode. Alternatively you can add/find your own subnodes in the OnLoad or OnSave methods.


    In this next example we\'ll add two modules, ModuleAnimatorLandingGear (from Example #1) and an as yet unknown module called ModuleAnimateHeat.
    [spoiler]
    Code:
    MODULE
    {
     name = ModuleAnimatorLandingGear
    }
    MODULE
    {
     name = ModuleAnimateHeat
     ThermalAnim = HeatAnimationEmissive;
    }
    [/spoiler]

    Here is ModuleAnimateHeat. This was written to handle all of emissive heat glowing by heated parts. The heat is represented in the models as animations going from 0->1 (none to full heat)
    [spoiler]
    Code:
    public class ModuleAnimateHeat : PartModule
    {
      [KSPField]
      public string ThermalAnim = 'HeatAnimationEmissive';
    
      public float draperPoint = 525f; // Draper point is when solid objects begin to emit heat.
    
      public AnimationState[] heatAnimStates;
    
      public override void OnStart(StartState state)
      {
        HeatEffectStartup();
      }
    
      public void Update()
      {
        UpdateHeatEffect();
      }
    
      private void HeatEffectStartup()
      {
        Animation[] heatAnims = part.FindModelAnimators(ThermalAnim);
        heatAnimStates = new AnimationState[heatAnims.Length];
    
        int i = 0;
        foreach (Animation a in heatAnims)
        {
          AnimationState aState = a[ThermalAnim];
          aState.speed = 0;
          aState.enabled = true;
    
          a.Play(ThermalAnim);
    
          heatAnimStates[i++] = aState;
        }
      }
    
      private void UpdateHeatEffect()
      {
        float temperatureValue = Mathf.Clamp01((part.temperature - draperPoint) / (part.maxTemp - draperPoint));
    
        foreach (AnimationState a in heatAnimStates)
        {
          a.normalizedTime = temperatureValue;
        }
      }
    }
    [/spoiler]



    Conclusion

    Well hopefully you learned vaguely how to use PartModule and ConfigNode from all that. It was all written to be really simple and thus I hope it turns out to be. I\'m always open to questions and feedback, send me a mail if you really need to. I will hold a few dev sessions over the coming weeks to get some feedback and answer more detailed questions.


    In part #2 i\'ll talk about resources, how to define them and how to use them.
    Si non confectus, non reficiat.

  4. #4

    Re: 0.15 code update - PartModule, KSPField, KSPEvent and ConfigNode

    PartResources

    The PartResources system is fairly simple. Its all fairly simple. Its designed to allow many addon devs to use a few core resource definitions, add them to parts easily and also retrieve them easily.


    Resource definitions

    ResourceDefinitions define a resource, in name and a few physical values. Resource definitions are currently defined in .cfg files in the /resources directory. It parses all .cfg files in resources at startup so filename is unimportant. As far as I know, no stock .cfg files come with KSP so lets look at mine.
    [spoiler]
    Code:
    RESOURCE_DEFINITION
    {
     name = LiquidOxygen
     density = 1.5
    }
    RESOURCE_DEFINITION
    {
     name = LiquidHydrogen
     density = 1.5
    }
    RESOURCE_DEFINITION
    {
     name = LiquidHydrazine
     density = 1.5
    }
    RESOURCE_DEFINITION
    {
     name = AtmosphereKerbin
     density = 0.1
    }
    [/spoiler]

    So we have four resource definitions there. All pretty simple.
    None of the densities are to scale, they\'re just for idle testing. The density should be in kg/m^3 but i\'m afraid KSP\'s mass units make no sence so they\'re in tonnes/m^3 I think (12 kilobanana per m^3). Going forward I would like to agree some standard units.

    Ok mass confusion (pun intended) over.


    Tanks for the resources

    To create a volume of resource on a part you can do it through the part.cfg file. Here is the cfg for a hydrazine fuel tank.
    [spoiler]
    Code:
    RESOURCE
    {
     name = LiquidHydrazine
     amount = 2
     maxAmount = 2
    }
    [/spoiler]

    Thus this part contains a node called RESOURCE which has a name, an amount value and a maxAmount value. The name should match the one in your resource definition and the amount/max are in units of volume. The mass increase on the part is calculated from the amount*density. Amount should be less than or equal to the max.


    Crossfeeding

    If a part contains a resource with a maxAmount of zero then this allows the part to act as a crossfeed for this resource. Alternatively, the standard 'fuelCrossFeed' acts as a master crossfeed switch. This was purely to ensure backward compatability with crossfeeding mods should we have upgraded our parts to use PartResource.


    RequestResource

    Parts have a method called RequestResource. This is the main method you will use to request or produce resources.
    public float RequestResource(string resourceName, float demand)

    Any part can request fuel in this way. A PartModule must use part.RequestResource but the operation is exactly the same. This method will return you an amount drawn through the system, if the amount is available. If the demand is negative then you are producing the resource and it will start to fill up reservoirs connected to the system. As the system is not balanced you must first create an amount somewhere in order to fill it up.


    Example #1 - Generic rocket module

    This is a rocket engine module that can run on any number of any propellants. Calling it a rocket is a bit of a misnomer, really its a generic reaction engine.
    The list of Propellant subclasses is loaded/saved manually in the OnLoad and OnSave. (I do hope you\'ve followed the PartModule tutorial above!)
    [spoiler]
    Code:
    public class ModuleEngineRocketLiquid : PartModule
    {
      [System.Serializable]
      public class Propellant
      {
        public string name;
    
        public int id;
    
        public float requirementMax;
    
    
        public float currentRequirement;
    
        public float currentAmount;
    
        public float currentPotential;
    
    
        public Propellant()
        {
        }
    
        public void Load(ConfigNode node)
        {
          name = node.GetValue('name');
          id = name.GetHashCode();
    
          if (node.HasValue('requirementMax'))
            requirementMax = float.Parse(node.GetValue('requirementMax'));
        }
    
        public void Save(ConfigNode node)
        {
          node.AddValue('name', name);
          node.AddValue('requirementMax', requirementMax);
        }
      }
    
      [KSPField]
      public float thrustMax;
    
      [KSPField]
      public string thrustVectorTransformName;
    
      [KSPField]
      public float currentThrottle;
    
    
      public List<Propellant> propellants;
    
      public Transform thrustTransform;
      public float currentThrust;
      public float currentPower;
    
    
      public override void OnAwake()
      {
        if (propellants == null)
          propellants = new List<Propellant>();
      }
    
      public override void OnStart(StartState state)
      {
        if (thrustTransform == null)
        {
          thrustTransform = part.FindModelTransform(thrustVectorTransformName);
        }
      }
    
      // calculate current output thrust as factor of fuel requirements met
      public override void OnUpdate()
      {
        //string str = '';
        if (part.isControllable)
        {
          currentThrottle = FlightInputHandler.state.mainThrottle;
        }
    
        float throttle = currentThrottle;
        currentPower = throttle;
    
        foreach (Propellant p in propellants)
        {
          p.currentRequirement = (p.requirementMax * throttle * Time.deltaTime);
    
          p.currentAmount = part.RequestResource(p.id, p.currentRequirement);
    
          p.currentPotential = p.currentAmount / p.currentRequirement;
    
          currentPower = Mathf.Min(currentPower, p.currentPotential);
        }
    
        currentThrust = thrustMax * currentPower;
      }
    
      public override void OnFixedUpdate()
      {
        if (thrustTransform != null && part.Rigidbody != null)
        {
          part.Rigidbody.AddForceAtPosition(-thrustTransform.forward * currentThrust, thrustTransform.position);
        }
      }
    
    
      public override void OnSave(ConfigNode node)
      {
        foreach (Propellant p in propellants)
        {
          p.Save(node.AddNode('PROPELLANT'));
        }
      }
    
      public override void OnLoad(ConfigNode node)
      {
        foreach (ConfigNode subNode in node.nodes)
        {
          switch (subNode.name)
          {
            case 'PROPELLANT':
              if (!subNode.HasValue('name'))
              {
                Debug.Log('Propellant must have value \'name\'');
                continue;
              }
              Propellant newProp = new Propellant();
              newProp.Load(subNode);
              propellants.Add(newProp);
              break;
          }
        }
      }
    }
    [/spoiler]

    The key line in all that is 'p.currentAmount = part.RequestResource(p.id, p.currentRequirement);' It requests a resource from its id (the GetHashCode of the resource name).


    Example #2 - Air intake module

    For the air-breathing engines to work properly here is a PartModule to do so. It creates an amount of resource per frame depending on forward velocity of the intake and also a mechanical suction effect.
    [spoiler]
    Code:
    public class ModuleResourceIntake : PartModule
    {
      [KSPField]
      public string resourceName = 'AtmosphereKerbin';
    
      [KSPField]
      public float area;
      public float airSpeed;
    
      [KSPField]
      public string intakeTransformName = 'Intake';
      public Transform intakeTransform;
    
      [KSPField]
      public bool intakeEnabled = true;
    
      [KSPField]
      public float intakeSpeed;
    
      [KSPField]
      public float intakePower = 75;
    
      public override void OnStart(StartState state)
      {
        if (intakeTransform == null)
        {
          intakeTransform = part.FindModelTransform(intakeTransformName);
        }
      }
    
      [KSPEvent(guiActive = true, guiName = 'Deactivate')]
      public void Deactivate()
      {
        Events['Deactivate'].active = false;
        Events['Activate'].active = true;
    
        intakeEnabled = false;
      }
    
      [KSPEvent(guiActive = true, guiName = 'Activate')]
      public void Activate()
      {
        Events['Deactivate'].active = true;
        Events['Activate'].active = false;
    
        intakeEnabled = true;
      }
    
      public override void OnFixedUpdate()
      {
        if (intakeEnabled)
        {
          if (part.isControllable)
          {
            intakeSpeed = FlightInputHandler.state.mainThrottle * intakePower;
          }
          airSpeed = (float)vessel.srf_velocity.magnitude;
          float aoa = Vector3.Dot(vessel.srf_velocity.normalized, intakeTransform.forward.normalized);
          float airVolume = (aoa * area * (airSpeed + intakeSpeed) * Time.fixedDeltaTime);
          float airMass = (float)vessel.atmDensity * airVolume;
          airMass = Mathf.Max(0f, airMass);
    
          part.RequestResource(resourceName, -airMass);
        }
      }
    
      public override void OnLoad(ConfigNode node)
      {
        if (intakeEnabled)
        {
          Events['Deactivate'].active = false;
          Events['Activate'].active = true;
        }
      }
    }
    [/spoiler]

    Note is uses a transform inside model to calculate relative velocities, this will take into account any mirroring that happens.

    A part.cfg using this module looks like this..
    [spoiler]
    Code:
    MODULE
    {
      name = ModuleResourceIntake
      resourceName = AtmosphereKerbin
      area = 2
      intakeTransformName = Intake
      intakeEnabled = True
      intakePower = 75
    }
    RESOURCE
    {
     name = AtmosphereKerbin
     maxAmount = 2
    }
    [/spoiler]

    We have created a resource tank on the part. This is akin to the compression chamber, it is constantly topped up with air (assuming engine is running or we have forward velocity).



    Conclusion

    So the resources system is incredibly simple at the moment. Really we need nothing more complicated however I would like to expand it to include flow, pressure, etc but have not had the time or the maths skills to do it. If anyone has any experience with iterative analysis of pipe networks and electrical circuits then give me a shout.
    Si non confectus, non reficiat.

  5. #5
    Junior Rocket Scientist N3X15's Avatar
    Join Date
    Dec 2011
    Posts
    605
    Blog Entries
    17

    Plugin Loading Architecture Changes for 0.15

    So, here we are again, another update behind us. Despite the fact that a lot of stuff got fixed, you\'re probably wondering why your plugins are starting to screech about namespaces being forbidden or something. Some of you are also probably about to write an angry post in the development forums regarding this; If you are, please stand back from the keyboard until you\'re through reading this post.

    First, a short explanation.

    When I first sent Squad the plugin loading code, I had intended it as a mere proof of concept. It was simple, and I fully expected Squad to change it and improve the various glaring security problems with it. It was changed so it would work with KSP, but the security problems were not addressed, primarily because Squad was in the middle of all the work for 0.14 and didn\'t have time to mess with it. Fortunately, we have not had any problems with malware yet, but we\'d rather not hold our breath, and we\'ve made security a priority.

    Starting in 0.15, specific namespaces will be forbidden at the bytecode level and plugins will fail to load if they use these namespaces. At the moment, the blacklist consists of System.IO and System.Xml, and this is to prevent plugins from accessing files outside of the KSP install folder. However, we realize that you all need to use persistant settings, so we\'ve made a really easy-to-use settings system available in the KSP.IO namespace. It uses XML at the moment, so it\'s not very fast, and it\'s clunky. It\'s more for serialization of simple values. I have also made file streams available for those of you who need the additional performance and flexibility. Everything ends up in the PluginData/yourplugindllname/ folder (PluginConfiguration uses config.xml, everything else is user-defined).

    We realize this isn\'t ideal, but this is the best we can do at the moment, and we will be iterating on it to improve it and add stuff you guys need for your plugins. Lots of stuff is planned for 0.16, including a bunch of changes to make flight association easier.

    In short, we\'ve added some security systems to KSP that will throw a fit when your plugin uses anything in System.IO or System.Xml. Use KSP.IO instead of System.IO and it should work again. Stop screaming, it\'ll get better.

    If you need any additional interfaces (such as the aforementioned streams), please let me know either by starting a development thread, or by bugging me in IRC.

    Documentation is available here.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •