Jump to content

Official PartModule Documentation


Mu.

Recommended Posts

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...


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)
{
}
}

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.


public class ModuleCommand : PartModule
{
public override void OnUpdate()
{
if (FlightInputHandler.state.gearDown || FlightInputHandler.state.gearUp)
{
part.SendEvent('InputGearToggle');
}
}
}

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.


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';
}
}
}

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..


/// <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 = '';
}
}

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..


/// <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 = '';
}
}

Edited by stupid_chris
Link to comment
Share on other sites

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.


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;
}
}

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.


/// <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);
}

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..


public class ConfigNode
{
public string name;
public List<ConfigNode> nodes;
public List<ConfigNode.Value> values;

public class Value
{
public string name;
public string value;
}
}

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..


[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);
}
}
}

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


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
}
}

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.


MODULE
{
name = ModuleAnimatorLandingGear
}
MODULE
{
name = ModuleAnimateHeat
ThermalAnim = HeatAnimationEmissive;
}

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)


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;
}
}
}

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.

Edited by stupid_chris
Link to comment
Share on other sites

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.


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
}

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.


RESOURCE
{
name = LiquidHydrazine
amount = 2
maxAmount = 2
}

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!)


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;
}
}
}
}

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.


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;
}
}
}

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..


MODULE
{
name = ModuleResourceIntake
resourceName = AtmosphereKerbin
area = 2
intakeTransformName = Intake
intakeEnabled = True
intakePower = 75
}
RESOURCE
{
name = AtmosphereKerbin
maxAmount = 2
}

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.

Edited by stupid_chris
Link to comment
Share on other sites

  • 2 weeks later...

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.

Link to comment
Share on other sites

Guest
This topic is now closed to further replies.
×
×
  • Create New...