What is CML?
CML is meant to be a replacement for XML and thus allows you to do pretty much the same as you would have done with XML, only more. CML allows you to turn your key-value pairs into typed data and thus you can use the CML file directly in your project as opposed to having to read and parse the file into custom prepared data structures.
What are the features of CML?
- Has a normal and simplified syntax you can swap between at any point
- Easier to read / write/ understand
- All data stored under categories
- Very forgiving syntax
- Nodes don’t have to contain the same fields. Only add what you need when you need it
- Load and save your data with a single line of code, respectively
- Runtime typed data creation
- Non-existing values return the default values for each type instead of throwing exceptions
- Each node can be a parent for child nodes thus you can have nested nodes as deep as you want
- Find nodes by:
- Direct access via index
- First node of type (optionally with offset)
- First node with specific field
- First node with specific field with specific value
- Children of a specific node
- Each node can have unlimited key-value pairs
- Since each node can be a parent/header node it means you can have different sections inside a single file, thus consolidating multiple files into one
- Available natively for Unity, Unreal Engine, .NET and php
Example CML file
<libraries>
<library>name=Uptown;Contact number=555 2552
<section>name=History
<book>name=American
<book>name=English
<book>name=African
<section>name=Fiction
<book>name=Space Adventures 2099
</section>
<section>name=Manga
<book>name=Akira
<book>name=Tetsuo
//this library entry has extra fields
<library>name=Downtown;demolition date=2029/10/07;Contact=Bill Murray; Contact number = 555 1525
<section>name=Science
<book>name=Apollo 13
<section>name=newspapers; type=subsection
<newspaper>name=Times
//This node uses the normal syntax in stead of simplified syntax
<paper>date=2021/10/10
[headline]Cubs win the world series
[number of pages]86
[copies sold]25625
</paper>
<paper>date=2021/10/11
[headline]Oops, our mistake
[number of pages]45
[copies sold]89940
We would like to apologize for that misinformation
printed in yesterday's paper.
<newspaper>name=Daily
<paper>date=2021/10/10
[headline]How to never lose at gambling
[copies sold]199991
[barcode]1251125425
The process of never losing at gambling is really quite simple
Don't gamble !
Example explained
Notice how there is no consistency in the above example and yet this is perfectly valid.
- The first
library
uses exclusively simplified syntax while the secondlibrary
mixes normal and simplified syntax - The second
library
lists more information about itself. Not all matching nodes need have the same fields - Only the second
library
has a subsection ofnewspapers
- Multiple data types in one file with different fields per type and per entry
- Trailing and preceding whitespace is automatically trimmed in regular and simplified syntax
- Closing tags are entirely optional
- Some
paper
nodes have additional data entries no other node has - Comments are supported in CML
How to use this example
Reading
All nodes are indexed sequentially starting from 0. By calling the Children() function and passing index 0 you will receive all children of node 0, which is “libraries
“. The first child is library
and thus it will return all library
nodes.
Now that you have a library
node (in this case the first node has an ID of 1) you can call the Children() function again, this time with the library
‘s ID to return all of the library’s immediate children. The first node it will find is section
and thus it will return all section nodes until it finds the next library
node. In this case it will return 2 section
nodes.
You could follow the same logic to find the newspapers if you knew in advance which nodes would follow which but since not all library section
s have a section called newspaper
here you have 2 options.
- call
AllNodesOfType("paper")
to fetch allpaper
nodes in the entire file - call
NodesWithField("type","subsection")
to find all nodes that has a field called “type” that has a value of “subsection”. Alternatively, since the newspapers node is the only one to contain the field “type” you could omit the value while still receiving the same result
Writing
Each node consists of 2 separate sets of data. The first and most common is the key-value pairs where the values can be turned into typed data at runtime. The second is a list of strings that are added in sequence, fetched sequentially and used as strings only.
The former can be set by calling the Set or SetX function based on the type of data you want to set (where X is the data type).
In Unity, for example, this would be Seti() for integers and Set() for almost any other type.
In the Unreal Engine, this would be Set() for FStrings and SetBoolean, SetRotator, SetVector etc respectively for each respective type
Strings are added to the string list named Data using the function AddData().
Advanced reading
What is important to note is that all data is saved as strings and thus anything you can turn into a string can be saved to a CML file. This includes base64 encoded binary data like graphics or mp3 files. When the data is fetched you can specify the type you want the value to be returned as. If the value cannot be returned as that type it will return the default value for that type. While it is not important for a field to actually exist when you try and use it, it is important for you to know the type of data a field should contain or else you may end up with unexpected results !
<mydata>
[age] 18
[can_vote] true
[name] Stormchaser
[url]aHR0cHM6Ly9teWJhZHN0dWRpb3MuY29t
[novalue]
Field | Type | Returns… |
age | float | 18.0 |
age | int | 18 |
age | boolean | true *1 |
can_vote | boolean | true *1 |
can_vote | int | 0 *2 |
can_vote | string | true |
can_vote | Vector3(Unity) | Vector3.zero |
name | string | Stormchaser |
name | boolean | true *1 |
name | Rect(Unity) | Rect(0,0,0,0) |
url | string | aHR0cHM6Ly9teWJhZHN0dWRpb3MuY29t |
url | Rotator(UE) | (0,0,0) |
url | Vector(UE) | (0,0,0) |
url | boolean | true |
novalue | float | 0.0f |
novalue | boolean | false |
novalue | Quaternion(Unity) | Quaternion.identity |
novalue | string(Unity) | string.Empty |
*1 Values larger than 0 and “true” return true. Anything else returns false
*2 “true” is not numeric