Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ SS14 By Example
- [Adding a Simple Bikehorn](en/ss14-by-example/adding-a-simple-bikehorn.md)
- [Making a Sprite Dynamic](en/ss14-by-example/making-a-sprite-dynamic.md)
- [Porting Appearance Visualizers](en/ss14-by-example/making-a-sprite-dynamic/porting-appearance-visualizers.md)
- [YAML Inheritance Guide](en/ss14-by-example/yaml-inheritance-guide.md)
- [Basic Networking and You](en/ss14-by-example/basic-networking-and-you.md)
- [Guide to Prediction](en/ss14-by-example/prediction-guide.md)
- [Fluent and Localization](en/ss14-by-example/fluent-and-localization.md)
Expand Down
293 changes: 293 additions & 0 deletions src/en/ss14-by-example/yaml-inheritance-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# YAML Inheritance Guide
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# YAML Inheritance Guide
# YAML Inheritance Guide
This guide will teach you how inheritance works in YAML prototypes. Inheritance allows "child" prototypes to inherit data from "parent" prototypes, making it easy to copy or change behaviors without needing to type out all data for every single prototype.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick summary setting expectations for what the doc will contain.

### Inheritance Rules

There are different ways DataFields get merged or overwritten when inheriting from another prototype in YAML and they can be a little confusing.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
There are different ways DataFields get merged or overwritten when inheriting from another prototype in YAML and they can be a little confusing.
There are different ways DataFields get merged or overwritten when inheriting from another prototype in YAML, and they can be a little confusing.

Let's look at a few examples:
```yml
# Define a few tags for testing purposes.
# Normally these should be in tags.yml.
- type: Tag
id: TagA1

- type: Tag
id: TagA2

- type: Tag
id: TagB

- type: entity
abstract: true
id: ParentA
components:
- type: Tag
tags:
- TagA1
- TagA2
- type: MeleeWeapon # turn the entity into a weapon
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- type: MeleeWeapon # turn the entity into a weapon
- type: MeleeWeapon # This component allows the entity to function as a weapon.

damage:
types:
Heat: 10

- type: entity
abstract: true
id: ParentB
components:
- type: Tag
tags:
- TagB
- type: PointLight # make it glow
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- type: PointLight # make it glow
- type: PointLight # This component makes the entity glow.

color: green
energy: 10

- type: entity
abstract: true
id: ParentC
components:
- type: Sprite
sprite: Objects/Fun/Plushies/hampter.rsi # change the sprite folder only, but not the state
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sprite: Objects/Fun/Plushies/hampter.rsi # change the sprite folder only, but not the state
sprite: Objects/Fun/Plushies/hampter.rsi # Change the sprite folder only, but not the state.


# A simple item with a lizard sprite.
# A sprite needs both the 'sprite' datafield, which contains the path to the rsi folder the image file is in,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# A sprite needs both the 'sprite' datafield, which contains the path to the rsi folder the image file is in,
# A sprite needs both the 'sprite' datafield, which contains the path to the RSI folder the image file is in, and the 'state' datafield which is the name of the .png file itself.

# and the 'state' datafield which is the name of the .png file itself.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# and the 'state' datafield which is the name of the .png file itself.

Should this be in a new line?

- type: entity
parent: BaseItem # This parent has a bunch of components giving it physics, a fixture and allowing us to pick it up..
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parent: BaseItem # This parent has a bunch of components giving it physics, a fixture and allowing us to pick it up..
parent: BaseItem # This parent has a bunch of components giving it physics and a fixture, allowing us to pick it up.

id: TestItem1
name: test item 1
description: lizard
components:
- type: Sprite
sprite: Objects/Fun/Plushies/lizard.rsi
state: icon # this state name is the same for all basic plushie sprites
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
state: icon # this state name is the same for all basic plushie sprites
state: icon # This state name is the same for all basic plushie sprites.


# The lizard sprite rsi is inherited first from TestItem1.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# The lizard sprite rsi is inherited first from TestItem1.
# The lizard sprite RSI is inherited first from TestItem1.

# The rsi is not overwritten by the hampter because it already exists in the first parent.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# The rsi is not overwritten by the hampter because it already exists in the first parent.
# The RSI is not overwritten by the hampter because it already exists in the first parent.

# So the resulting entity has a lizard sprite.
- type: entity
parent: [ TestItem1, ParentC ]
id: TestItem2
name: test item 2
description: lizard

# If we inherit in this order the rsi path will be taken from ParentC first.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# If we inherit in this order the rsi path will be taken from ParentC first.
# If we inherit in this order the RSIpath will be taken from ParentC first.

# TestItem1 will then add the state Datafield, but not overwrite the rsi.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# TestItem1 will then add the state Datafield, but not overwrite the rsi.
# TestItem1 will then add the state Datafield, but not overwrite the RSI.

# The result will be a hampter.
- type: entity
parent: [ ParentC, TestItem1 ]
id: TestItem3
name: test item 3
description: hampter

# This time we manually overwrite the inherited rsi.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# This time we manually overwrite the inherited rsi.
# This time we manually overwrite the inherited RSI.

# The inherited state remains unchanged.
# The result will be another hampter.
- type: entity
parent: TestItem1
id: TestItem4
name: test item 4
description: hampter
components:
- type: Sprite
sprite: Objects/Fun/Plushies/hampter.rsi

# This item will inherit the tags from ParentA, but not from ParentB.
# So it will have TagA1 and TagA2.
# The item will have both the PointLightComponent and the MeleeWeaponComponent and the corresponding DataFields set in the parents.
- type: entity
parent: [ TestItem1, ParentA, ParentB ]
id: TestItem5
name: test item 5
description: lizard

# To fix this and make the entity have all 3 tags we have to redefine the list manually.
- type: entity
parent: [ TestItem1, ParentA, ParentB ]
id: TestItem6
name: test item 6
description: lizard
components:
- type: Tag
tags: # this overwrites the inherited list
- TagA1
- TagA2
- TagB
```


To summarize the inheritance rules:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this section before the yaml example. That way you prime the reader for what they will see in the example, rather than show a lot of yaml without it being clear what they should be looking for.

You do this for the next sections and I think they read smoother as a result.

1. A prototype can have one or multiple parents.
2. A DataField that is not set in YAML gets its default value from its C# definition. In C# all variables have a default value if not specified otherwise, for example `public bool SomeVariable;` will always be `false` (in other programming languages you may get random bits).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. A DataField that is not set in YAML gets its default value from its C# definition. In C# all variables have a default value if not specified otherwise, for example `public bool SomeVariable;` will always be `false` (in other programming languages you may get random bits).
2. A DataField that is not set in YAML gets its default value from its C# definition. In C#, all variables have a default value if not specified otherwise; for example, `public bool SomeVariable` will always be `false` (in other programming languages, you may get random bits).

3. If you inherit from multiple parents, then components and their DataFields are inherited individually.
4. The order of inheritance matters: Each DataField will be taken from the first parent that has it set in YAML.
5. You can overwrite inherited DataFields by reassigning a new value in the child.
6. If a DataField is overwritten, then the whole instance of the variable is reassigned. This means data types like lists (for example for tags) won't get merged, but replaced. However, you can change this behaviour for individual DataFields using the `AlwasPushInheritance`attribute (see further below for a detailed explanation).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
6. If a DataField is overwritten, then the whole instance of the variable is reassigned. This means data types like lists (for example for tags) won't get merged, but replaced. However, you can change this behaviour for individual DataFields using the `AlwasPushInheritance`attribute (see further below for a detailed explanation).
6. If a DataField is overwritten, then the whole instance of the variable is reassigned. This means data types like lists (for example, tags) won't be merged; they'll be replaced. However, you can change this behaviour for individual DataFields using the `AlwasPushInheritance` attribute (see further below for a detailed explanation).

7. You cannot remove components that are inherited from a parent. You will have to make another abstract parent without that component instead to avoid copy pasting everything.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
7. You cannot remove components that are inherited from a parent. You will have to make another abstract parent without that component instead to avoid copy pasting everything.
7. You cannot remove components that are inherited from a parent. You will have to make another abstract parent without that component instead to avoid copy-pasting everything.


```admonish warning
Common Mistake:
Rule 6 often gets overlooked for tags. If you add a new tag to an EntityPrototype make sure that
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Rule 6 often gets overlooked for tags. If you add a new tag to an EntityPrototype make sure that
Rule 6 often gets overlooked for tags. If you add a new tag to an EntityPrototype make sure that:

- the new list of tags for that entity contains all tags that were previously inherited.
- any child prototypes that have their own tags explicitly list the new tag as well.
```

### Abstract Prototypes
If you got a base prototype that should not a fully functioning prototype on its own, but used for inheritance, then make sure to mark it as `abstract`. This will make sure it cannot be spawned by any means, it will be hidden from the F5 spawn menu, and will be ignored by integration tests.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you got a base prototype that should not a fully functioning prototype on its own, but used for inheritance, then make sure to mark it as `abstract`. This will make sure it cannot be spawned by any means, it will be hidden from the F5 spawn menu, and will be ignored by integration tests.
If you have a base prototype that should not be a fully functioning prototype on its own, but is used for inheritance, then make sure to mark it as `abstract`. This will ensure it cannot be spawned by any means; it will be hidden from the F5 spawn menu and ignored by integration tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add "only" to but is only used for inheritance. Afaik there is no reason to make something abstract if not for inheritance.

Example:
```yml
# A simplified plushie that inherits from BaseItem and has incomplete SpriteComponent DataFields.
- type: entity
abstract: true # should not be spawned since this prototype is incomplete
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
abstract: true # should not be spawned since this prototype is incomplete
abstract: true # Should not be spawned since this prototype is incomplete.

parent: BaseItem
id: BasePlushie
components:
- type: Sprite
state: icon # all plushie sprites have the same state name
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
state: icon # all plushie sprites have the same state name
state: icon # All plushie sprites have the same state name.

- type: EmitSoundOnUse # make it squeak when used
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- type: EmitSoundOnUse # make it squeak when used
- type: EmitSoundOnUse # Makes the plushie squeak when used.

sound:
collection: ToySqueak

# A plushie that can be spawned in the game.
- type: entity
parent: BasePlushie
id: PlushieBee
name: bee plushie
components:
- type: Sprite
state: plushie_h # add a state (the sprite path is inherited from BasePlushie)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
state: plushie_h # add a state (the sprite path is inherited from BasePlushie)
state: plushie_h # Adds a state (the sprite path is inherited from BasePlushie).


# And another one.
- type: entity
parent: BasePlushie
id: PlushieLizard # Weh!
name: lizard plushie
components:
- type: Sprite
state: plushie_lizard
```

If a prototype that is a fully functioning entity on its own should only be hidden from the F5 spawn menu, but can still be spawned through other means in-game, then you should be using The `HideSpawnMenu` category.
Examples are mind entities, objectives, or visual effects like this one used for flashbangs:
```yml
- type: entity
id: GrenadeFlashEffect
categories: [ HideSpawnMenu ]
components:
- type: PointLight
enabled: true
radius: 5
energy: 8
netsync: false
- type: LightFade
duration: 0.5
- type: TimedDespawn
lifetime: 0.5
```
This is a simple pointlight that quickly disappears and deletes itself. We use `categories: [HideSpawnMenu]` to make sure it does not show up in the spawn menu, but it can still be spawned through code using the `Spawn(protoId, coords)` method or similar.
This will also exclude it from several integration tests, which may otherwise fail for such an entity.

### Making a prototype inheritable
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Making a prototype inheritable
### Making a Prototype Inheritable

The above examples were all for `EntityPrototype`s, but they work the same for any other type of prototype. You can define your own prototype in C# and make it store DataFields. To make it inheritable in YAML you will have to implement the `IInheritingPrototype` interface. This requires a DataField for the parents and a bool for being abstract.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The above examples were all for `EntityPrototype`s, but they work the same for any other type of prototype. You can define your own prototype in C# and make it store DataFields. To make it inheritable in YAML you will have to implement the `IInheritingPrototype` interface. This requires a DataField for the parents and a bool for being abstract.
The above examples were all for `EntityPrototype`s, but they work the same for any other type of prototype. You can define your own prototype in C# and make it store DataFields. To make it inheritable in YAML, you will have to implement the `IInheritingPrototype` interface. This requires a DataField for the parents and a bool for being abstract.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also make a short mention of why you would even define your own prototype to begin with (e.g. mention how prototypes are useful as presets).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires a DataField for the parents and a bool for being abstract. ->
This requires a DataField for the ID and parents, and a bool for being abstract.

Example:
```csharp
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;

/// <summary>
/// A simple inheritable prototype for testing purposes.
/// </summary>
[Prototype]
public sealed partial class InheritanceTestPrototype : IPrototype, IInheritingPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; private set; } = default!;

/// <inheritdoc/>
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<InheritanceTestPrototype>))]
public string[]? Parents { get; private set; }

/// <inheritdoc/>
[NeverPushInheritance]
[AbstractDataField]
public bool Abstract { get; private set; }

/// <summary>
/// A string you can set in YAML.
/// </summary>
[DataField]
public string Field1 = "Field1 default value";

/// <summary>
/// And a second one.
/// </summary>
[DataField]
public string Field2 = "Field2 default value";

/// <summary>
/// A datafield with different inheritance behaviour.
/// This List will be merged instead of overwritten.
/// </summary>
[DataField, AlwaysPushInheritance]
public List<string> AlwaysInheritedField = new();

/// <summary>
/// A datafield with different inheritance behaviour.
/// This one will never get inherited.
/// </summary>
[DataField]
public string NeverInheridedField = "default value";
}
```

```yml
- type: inheritanceTest
id: Parent1
field1: foo

- type: inheritanceTest
id: Parent2
field2: bar

- type: inheritanceTest
parent: [ Parent1, Parent2 ]
id: Child
# This will inherit both field1 being foo, and field2 being bar.
```

### AlwaysPushInheritance, NeverPushInheritance
These two attributes can change the inheritance behaviour for a specific DataField. This works both for DataFields inside Components and inside custom Prototypes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
These two attributes can change the inheritance behaviour for a specific DataField. This works both for DataFields inside Components and inside custom Prototypes.
These two attributes can change the inheritance behaviour for a specific DataField. This works both for DataFields inside Components and inside custom Prototypes. You can find if a DataField has these attributes by looking at it in the C# code.

I'd move this comment up here instead of the snippet at the bottom.


`AlwaysPushInheritance` will make `List`s, `HashSet`s and `Dictionaries` get merged instead of overwritten.
```yml
- type: inheritanceTest
id: Parent1
alwaysInheritedField:
- foo

- type: inheritanceTest
id: Parent2
alwaysInheritedField:
- bar

- type: inheritanceTest
parent: [ Parent1, Parent2 ]
id: Child
alwaysInheritedField:
- weh
```
The `Child` prototype will have a `AlwaysInheritedField` DataField with a list containing all three strings `foo`, `bar` and `weh`.
This comes at the downside of not being able to remove any entries from that list in any inheritors. Ideally in the future we will add a more powerful YAML syntax to allow us to select the inheritance behaviour on a case by case basis (see some discussion [here](https://github.com/space-wizards/RobustToolbox/issues/5141), [here](https://github.com/space-wizards/space-station-14/issues/43326) and [here](https://forum.spacestation14.com/t/move-tags-to-rt-add-proper-entity-prototype-syntax/22759)).
This also works recursively for any custom `DataDefinition`s, see for example the `Solution` DataField in the `SolutionComponent`.

`NeverPushInheritance` will make a DataField not get inherited at all.
```yml
- type: inheritanceTest
id: Parent
neverInheridedField: weh

- type: inheritanceTest
parent: Parent
id: Child
# neverInheritedField will be the C# default "default value" again
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# neverInheritedField will be the C# default "default value" again
# neverInheritedField will be the C# default "default value" again.

```

So it's recommended to always take a look at a component's or prototype's C# definition to see which attributes their DataFields have and which inheritance behaviour they will follow.
Loading