Skip to content

General Configuration Information

Steank edited this page Nov 21, 2023 · 26 revisions

Introduction

In Phantazm, many gameplay aspects are configurable, that is, they are defined by a set of human-readable configuration files. This approach is preferable to baking such details into the code, as it becomes possible for anyone with access to the files to adjust values easily without needing specialized skills.

Basic YAML

Configuration files can exist in a number of different formats. The format used for most Phantazm configuration is called YAML, which is a recursive acronym meaning "YAML Ain't Markup Language". YAML files end in either .yml or .yaml.

lineOfText: "This is a line of text in YAML"
lineOfText2: You can write text without quoting it unless you use certain special characters!
listOfValues: [ this is the first value, this is the second value ]
number: 100
subConfiguration:
  subValue: 10
  subText: this is a bit of nested configuration
listOfObjects:
  - name: This is the first in a list of objects!
    value: 10

  - name: This is the second in a list of objects!
    value: 11

The core concept of YAML (or any configuration format for that matter) is the key-value pair, which can be thought of as some arbitrary value (types of values include text, a number, a list of values, more key-value pairs, etc.) that is associated with a single key, acting as its name.

The key is separated from the value by a colon : character, followed by a space. For example:

isInstaKill: true

Here, the key (name) isInstaKill is associated with the value true. One rule to keep in mind: you (normally) can't have two identical keys in the same indentation level, like below:

isInstaKill: true
isInstaKill: false

Your keys also shouldn't contain spaces. By convention, keep the first letter of the key lowercase, with the first letter of each subsequent word uppercase (this is also known as camelCase).

Lists of values can be defined using square brackets [ and ] and separated by commas ,, or on separate lines by adding the - character before the value.

playersInGame: [ "Steank", "TachibanaYui", "Typhoon_Alex" ]
playersInGameOnSeparateLines:
  - "Steank"
  - "TachibanaYui"
  - "Typhoon_Alex"

Keys can point to additional blocks of configuration. This is used to categorize and organize related groups of values. For example:

powerupStates:
  instaKill: true
  doubleGold: true
  extraHealth: false

livingPlayers: [ "Steank", "TachibanaYui", "Typhoon_Alex" ]

"Blocks of configuration" are also referred to in this documentation as objects. You can have lists of objects, too:

players:
  - name: "Steank"
    coins: 1000
    isAlive: true

  - name: "TachibanaYui"
    coins: 1000
    isAlive: true

  - name: "Typhoon_Alex"
    coins: 1000
    isAlive: false

Note the presence of a - character before the first key-value pair in every configuration block. This indicates the block is a member of a list. Therefore, it's acceptable for the keys name, coins, and isAlive to be repeated, even though they are the same indentation level, since they are actually all in their own separate blocks. However, the following example is not valid:

players:
  - name: "Steank"
    coins: 1000
    isAlive: true

  - name: "TachibanaYui"
    coins: 1000
    coins: 2000
    isAlive: true

  - name: "Typhoon_Alex"
    coins: 1000
    isAlive: false

The second entry in the list has the coins key twice, which is not allowed.

Now that you understand the basic grammar, it is important to understand the concept of values having types. What type you want your value to be will dictate how you define it. For example, if you are trying to define a numeric value, you must not add quotation marks or other extraneous characters. Each type, and how to define them, are listed below.

Type Examples Notes
Text
name: 'this is some text'
otherText: this is some unquoted text
otherOtherText: "this is some double-quoted text"

You don't need to quote text with ', you can use " or nothing at all instead, but due to reasons that will be explained later, you should use ' for every string unless you know what you're doing. This data type is sometimes referred to as a string.

Number
value: 10
decimalValue: 1.5
Numbers must be unquoted.
Boolean
value: true
otherValue: false
Boolean values are simply binary "yes" or "no" conditions. They must be un-quoted in order to interpret correctly.
List
values: [ 10, 'some text' ]
otherValues:
  - 10
  - 'some text'

Lists can either be defined using [ and ], and separating entries with ,, or they can be defined with each entry on a separate line starting with -.

Blocks
values: 
  isOnFire: true
  height: 2
  width: 1

Blocks must be indented by exactly 2 spaces, and each key-value pair of the indented block must be at the same indentation level as the others. Blocks are sometimes referred to as maps, and are said to be "nested", which references how they are indented inside some other area in the configuration file.

Finally, YAML configuration has the concept of comments. Comments are notes you may leave for yourself or others to clarify what something is doing. Comments are indicated with a # character and cause everything after the symbol to be ignored by the computer. You are encouraged to leave descriptive comments whenever possible!

# This configuration file defines how many coins Steank has!
# This is an example to indicate how to use YML and isn't possible to use for an actual Zombies map.
name: "Steank" # The player's name
coins: 1000 # How many coins they have
isAlive: true # Whether or not they're alive

This concludes the basic knowledge you'll need to understand how YML files are structured. However, you should know that the format features some more advanced concepts that won't be covered here. It also has some pitfalls, which the next section will show you how to mitigate.

YAML Pitfalls

Most of the "issues" with YAML center around two concepts: indentation, and how text values are handled. We'll cover each of them in turn and demonstrate how to avoid running into problems when designing configuration.

Indentation

Most YAML documents, except for the most simple, will make some use of nested configuration blocks like below:

equipmentName: pistol
stats: # Stats defines a nested configuration block
  reloadTime: 10
  maxAmmo: 50
  maxClip: 16

When working with these, it is extremely important to keep the indentation consistent. Take the below example:

equipmentName: pistol
stats: # Stats defines a nested configuration block
  reloadTime: 10
  maxAmmo: 50
 maxClip: 16 # maxClip is less indented than its neighbors so this file won't work!

Indented key-value pairs should have exactly 2 spaces before them, per each level of indentation needed. However, there is one exception to this, when lists of blocks are involved:

players:
  - name: "Steank" 
    coins: 1000
    isAlive: true

  - name: "TachibanaYui"
    coins: 1000
    coins: 2000
    isAlive: true

  - name: "Typhoon_Alex"
    coins: 1000
    isAlive: false

When defining these, include 2 spaces before the - symbol, then a single additional space, then the key-value pair. Subsequent key-value pairs in the same block should be aligned with the first.

Handling of text values

Throughout this tutorial, text has been shown both with and without quotes " or '. This is arbitrary. in YAML, you can define text either way. However, there are certain problems that can occur if you do not quote text. For one, if your text consists of entirely numerical digits, the computer will assume you mean to define a number rather than some text that happens to contain a number.

name: 123456 # Interpreted as the number 123456! This can cause issues

To fix this, put the value in quotes:

name: "123456" # This will be read as text, not a number, so we're good

Of course, if you're defining something like the amount of ammo a gun has, you will want your values to be interpreted as numbers, and so you should leave the quotes off.

In addition to numbers, there are other problematic values to watch out for when writing text. These include:

  • true, false, yes, and no
  • Text containing special values at the start of the text, like @, :, and many others not listed here
  • Text containing " or '

As you can see, there's a lot of caveats to consider. However, a simple solution is to just quote all text using single quotes, like this:

text: 'This is a quoted string! It can contain special characters, "even quotation marks"'

If you want the text to contain a single quote, just type two of them together: ''. Each pair of quotes inside of some text will be read as only one that is part of the string.

Summary

  • Configuration files consist of key-value pairs.
  • Key-value pairs consist of a name and a value.
  • Keys start with lowercase values, shouldn't contain spaces, and are separated from the value using a :
  • Values can be one of several different types:
    • Text (strings)
    • Numbers
    • Booleans (true or false)
    • Lists
    • Blocks (maps)
  • Indentation must be kept consistent to avoid issues.
    • Use 2 spaces for each level of indentation
    • When indenting lists of blocks, keep the key-value pairs of the block aligned with each other
  • Text should generally be single-quoted, 'like this', unless you know what you're doing.

Element

Element is the name of a bespoke software library Phantazm uses for defining certain types of complex behaviors in configuration files, so that they can be easily edited and modified without having to dig into the code. Elements, or element objects, refer to individual configuration blocks that define a particular aspect of gameplay.

Element is still in an early stage of development. This document will be updated as necessary to include any changes that impact configuration.

Simple element configuration

Here is an example of an actual element block used as part of powerup configuration:

type: zombies.powerup.action.modify_windows
radius: 10
shouldBreak: true

Let's break down how this is structured. All element objects contain a key named type. type is a string that defines what element you're trying to use. You can't use any string you want; they are defined in the code, and each does a different thing. For example, zombies.powerup.action.modify_windows breaks or repairs windows in a given radius (measured from the powerup).

After the type key comes the configuration of the element, known as its fields. Here is where you choose parameters that influence how the element behaves. In this case, you can choose to either break or repair windows, and specify a radius inside which this effect will take place.

However, not all elements are as simple as this. If they were, the configuration system wouldn't be very powerful! Certain element types allow you to define complex behavior by linking to other elements using paths.

Complex element configuration

Here's another actual example, this time from shop configuration:

type: zombies.map.shop.predicate.interacting
delegatePath: ./delegate
interactorPaths: [ ./firstInteractor, ./secondInteractor ]

delegate: 
  type: zombies.map.shop.predicate.uuid
  uuids: [ '6458e77a-f565-4374-9de7-c2a20be572f3' ]
  blacklist: false

firstInteractor:
  type: zombies.map.shop.interactor.messaging
  messages: [ 'You're Steank, and you activated this shop.' ]
  broadcast: false

secondInteractor:
  type: zombies.map.shop.interactor.messaging
  messages: [ 'Steank activated a completely useless shop.' ]
  broadcast: true

First, a bit of clarification. The element zombies.map.shop.predicate.interacting is a shop predicate. What shop predicates are is explained in detail in the shop configuration page. Simply put, they are conditions that are checked in order to determine if a player attempting to activate a shop should be able to do so. zombies.map.shop.predicate.interacting is a special kind of predicate that checks another predicate, and, if the delegate passes its check, activates an interactor. Shop interactors are elements that perform an action, which can be anything from giving the player equipment, taking coins from them, or broadcasting a message in chat.

Now let's break down what the above example actually does. We have the element zombies.map.shop.predicate.interacting, which delegates to an element of type zombies.map.shop.predicate.uuid, which is also a shop predicate. This one checks the UUID of the activating player against a list of configurable UUIDS, which can function as either a whitelist or a blacklist. This one has blacklist: false, so it acts as a whitelist, meaning it will only succeed if activated by a player with the UUID 6458e77a-f565-4374-9de7-c2a20be572f3.

Let's assume the activating player has the right UUID, so the predicate does pass. Then, the element zombies.map.shop.predicate.interacting moves on to activate the interactors. There are two of them defined: firstInteractor and secondInteractor. They are activated in the order in which they appear in the configuration file.

firstInteractor is a shop interactor of type zombies.map.shop.interactor.messaging, which sends a message in chat. It has broadcast: false, meaning it only sends a message to the activating player.

secondInteractor is another shop interactor, also of type zombies.map.shop.interactor.messaging. It sends a different message, and has broadcast: true, meaning it sends the message to all players in the game.

So, assuming the activating player has the right UUID, they will receive the message You're Steank, and you activated this shop. in chat, and everyone in the game will receive the message Steank activated a completely useless shop..

However, if the activating player did not have the right UUID, nothing would happen, because that is how zombies.map.shop.predicate.interacting works -- it only calls its interactors if its linked predicate succeeds.

Element paths

The previous example showed a lot of linked elements. One element may link to another by using a specially-formatted path string. Path strings are like filesystem paths, but for configuration files. A path consists of various names separated by a forward slash /. Each name can refer to a key, an index of a list, or it can be a special command. There are two recognized commands: . and ... Both are used to make a path relative to the element it is a part of.

Relative vs Absolute Paths

Paths can be either relative or absolute. A path is relative if it starts with a . or ... A path is absolute if it starts with any other character.

Absolute paths start at the root of the configuration. This is usually the top level of the file the element is contained in:

someNestedElement:
  type: zombies.map.shop.predicate.interacting
  delegatePath: /parent/delegate
  interactorPaths: [ ]

parent:
  delegate: 
    # ...

delegatePath starts with a /, so it looks for a key named parent at the root of the configuration. In the case of shops, this is the outermost indentation level of the file.

parent exists, so it then looks for a key named delegate inside parent. Such a key exists, and there is are no more path names to check, so we've located our linked element.

Now, a simple relative path:

someNestedElement:
  type: zombies.map.shop.predicate.interacting
  delegatePath: ./parent/delegate
  interactorPaths: [ ]

  parent:
    delegate: 
      # ...

This time, delegatePath starts with a .. This command essentially means "check for the next path key in the same indentation level as the current element". So, with that in mind, we check for a key named parent at the same indentation level as our element. As you can see, such a key exists, so we move on as usual, checking for a key named delegate inside parent. There is one, so we successfully located our linked element.

Finally, the other kind of relative path:

outer:
  someNestedElement:
    type: zombies.map.shop.predicate.interacting
    delegatePath: ../parent/delegate
    interactorPaths: [ ]

  parent:
    delegate: 
      # ...

Firstly, the path starts with .., so it means we're evaluating a path relative to the element in which it is defined. .. means "check for the next path key an indentation level below the current element". With that in mind, we check for a key named parent. The indentation level of our current element is 2, so we want to check for parent at indentation level 1. It exists, so we move on to delegate, now searching inside parent, and we've found our element.

Note: When using a path to refer to an index of a list, the first element of the list can be accessed with the name 0, the second 1, and so on.

Justification

Paths are useful because they allow you to structure and organize your configuration files however you want. It also reduces potential duplication: you may want to have two different elements link to the same element. Since this "structure" is defined in configuration and not Phantazm's code, it can be changed more easily, on a per-file basis, without the risk of breaking other files.

Summary

  • "Elements" are blocks of configuration that define specific behaviors
    • All elements have a type field, which defines what they do
    • Their behavior can be further customized by modifying their fields
  • Elements can link to other elements using paths.
    • Path can be either relative or absolute
    • Absolute paths start at the lowest indentation level (0 indentation)
    • Relative paths starting with only . are evaluated from the indentation level of the element they are contained in
    • Relative paths starting with .. are evaluated from one indentation level lower than the element they are contained in
  • Documentation for all of the element types, as well as what they do, can be found in their appropriate wiki pages