# This is an example of the nyan language
# The syntax is very much Python.
# But was enhanced to support easy hierarchical data handling.
# A nyan::Object is created easily:
ObjName():
member : TypeName = value
member_name : Object
...
Inherited(ObjName, OtherObj, ...):
member += 10
ObjName.member_name = "stuff"
PatchName<TargetNyanObject>[+AdditionalParent, +OtherNewParent, ...]():
member_to_modify = absolute_value
member_to_update += relative_value
member_to_replace @+= relative_value
member_to_replace_too @= absolute_value
ParentObject():
NestedObject(Inherited):
another_member : type_name = value
...
some_member : Inherited = NestedObject
...
An object declares a named storage space for key-value pairs.
ObjName(ParentObj, OtherParentObj, ...):
member_name : type_name
other_member_name : type_name = value
...
ParentObj.member_name = value
...
NestedObject():
another_member : type_name = value
...
The declaration of the object contains:
- A name for the object
- An arbritrary number of references to parent objects
- An arbritrary number of member definitions
- Initialization of or changes to inherited members
- An arbritrary number of declarations of nested objects
The object name should use CamelCase
notation.
All objects have a type whose name is equal to the object name. Additionally,
a nyan object inherit all types of their parents. For example, an object B(A)
has types B
and A
. Furthermore, all objects implicitly inherit from a
built-in object called Object
, even if no parent is directly defined.
Members defined by the object must have a unique name. The member name should
use snake_case
notation. In addition to the members defined by the object itself,
an object inherits all members of its parents. The value of inherited members
can be changed by the inheriting object, but their type cannot. References to
inherited members should be prefixed with the name of the ancestor object they are
inherited from. The prefix is optional if the inherited member's name does
not match any name of the unique members of the object.
Nested objects function like any other object. They allow for additional structuring of the data model using the object hierarchy.
An object is abstract when it contains at least one uninitialized member. This includes inherited members.
If an object contains no member definitions, changes to inherited members
or nested objects, the object definition body must contain the pass
keyword
instead.
ObjName(ParentObj, OtherParentObj, ...):
pass
Objects can be referenced by a fully qualified object name (FQON). The FQON is a unique string indentifier composed of the namespace the object name. The namespace of an object is be derived from the location of the file it is located in and its nesting parents (more details in section Namespace).
# file location: game/units/example.nyan
# FQON: game.units.example.ObjName
ObjName():
...
# FQON: game.units.example.ObjName.NestedObject
NestedObject():
...
# FQON: game.units.example.ObjName.OtherNestedObject
OtherNestedObject():
...
# FQON: game.units.example.ObjName.OtherNestedObject.DeepNestedObject
DeepNestedObject():
...
A member is a storage variable wth a predefined type.
A member is created by declaring it with a name and a data type. The type cannot be changed once declared.
SomeObject():
member_name : type_name
A member is initialized by declaring it and assigning a value to it. The value must correspond to the assigned type. For a full list of data types and their possible values see the member types doc.
SomeObject():
member_name : type_name = value
If the member was only declared when it was created, any child object can still initialize it by assigning a value.
SomeObject():
member_name : type_name
ChildObject(SomeObject):
SomeObject.member_name = value
A child object can also assign a new value to an inherited member that was already initialized. This change is not backpropagated to the parent, so other children are not affected.
SomeObject():
# Declaration and initialization: member_name = value
member_name : type_name = value
ChildObject(SomeObject):
# Assigns a new value: member_name = new_value
SomeObject.member_name = new_value
OtherChildObject(SomeObject):
# Uses value of parent: member_name = value
pass
A child object may use other type-specific operations than assignment on an initialized inherited member. Most of the time, these are relative operations.
The below example shows the Addition operation for the int
data type.
For a full list of data types and their possible operators see the
member types doc.
SomeObject():
# Declaration and initialization
member_name : int = 10 # result: member_name = 10
ChildObject(SomeObject):
# Adds 5 to the parent's member value
SomeObject.member_name += 5 # result: member_name = 10 + 5 = 15
OtherChildObject(SomeObject):
# Uses value of parent
pass # result: member_name = 10
After a member has been initialized (or changed by inheritance), the only way to alter a member value is through the application of a patch.
A patch is a special object type that can change member values of another object and add new inheritance parents to it.
A patch always targets a specific object. The object must be defined. Since patches are also objects, patches can target other patches and themselves.
A patch is defined like a normal object with the addition of a target. The patch target must be written in angle brackets after the object name. The defined target cannot be changed.
SomeObject():
...
PatchName<SomeObject>():
...
A patch can modify its target by:
- Changing a member value with an (type-specific) operator
- Replacing an operator of a member operation
- Adding additional parent objects
A patch cannot:
- Define its own members
- Add new members to the patch target
- Remove members from the patch target
- Redefine the data type of a member from the patch target
- Initialize members of the patch target
- Remove parents from the patch target
Member values are changed by using a type-specific operation on
the defined member. The below example shows the Assignment and
Addition operations for the int
data type. For a full list of data
types and their possible operators see the member types doc.
A patch can be applied multiple times. On every application all operations are executed.
SomeObject():
member_name : int = 7
other_member : int = 23
SomePatch<SomeObject>():
# Assigns 50 to member_name (on every application)
member_name = 50 # result: member_name = 50
# Adds 19 to other_member (on every application)
other_member += 19 # result: other_member = 23 + 19 = 42 (1st application)
# result: other_member = 42 + 19 = 61 (2nd application)
# result: other_member = 61 + 19 = 80 (3rd application)
# ...
Patches can target other patches in the same way.
SomeObject():
member_name : int = 7
other_member : int = 23
# Targets SomeObject
SomePatch<SomeObject>():
member_name = 50
other_member += 19
# Targets SomePatch
OtherPatch<SomePatch>():
# Adds 10 to the value that SomePatch assigns to member_name
member_name += 10 # resulting operation: member_name = 60
# Subtracts 7 from the value that SomePatch add to other_member
other_member -= 7 # resulting operation: other_member += 12
It should be stressed that by default, an operation only changes the member value and not the operator. More specifically, the operator of the changed member is not taken into account at all.
SomeObject():
member_name : int = 7
SomePatch<SomeObject>():
member_name -= 3
OtherPatch<SomePatch>():
# Adds 10 to the VALUE that SomePatch assigns to member_name
member_name += 10 # resulting operation: member_name -= 13
# it DOES NOT result in: member_name += 7
However, it is possible to override the operator and the value
of a target's operation. To do this, the patch operator must be prefixed
with the @
symbol.
SomeObject():
member_name : int = 7
SomePatch<SomeObject>():
member_name -= 3
OtherPatch<SomePatch>():
# Replaces the whole operation on member_name in SomePatch
member_name @+= 10 # resulting operation: member_name += 10
Overrides can be chained by adding multiple @
override symbols.
SomeObject():
member_name : int = 7
SomePatch<SomeObject>():
member_name -= 3
OtherPatch<SomePatch>():
member_name = 1
FixOtherPatch<OtherPatch>():
# Replaces the whole operation on member_name in SomePatch
member_name @@+= 5 # resulting operation (OtherPatch): member_name @+= 5
# resulting operation (SomePatch): member_name += 5
In the above example, the first @
in FixOtherPatch
marks an
override. All following @
s are considered parts of the overriden
operation, which means they are copied along with the operator and value
when the patch is applied. Therefore, the application of FixOtherPatch
will override the operation in OtherPatch
with member_name @+= 5
.
When OtherPatch
is applied after that, it will now override the
operation in SomePatch
with the operation defined after the @
. The
result for SomePatch
will be member_name += 5
.
A patch adds additional parent objects to its target by specifying
a list of object references after the patch target. The patch must
specify for every parent whether it is appended to the front or the
end of the list of parents. This is done by prefixing (append end)
or suffixing (append front) the +
symbol to the object reference.
SomeObject():
...
PatchName<SomeObject>[+AdditionalParent, AnotherParent+, ...]():
...
Adding a parent must not induce name clashes of members (see the multiple inheritance example).
Any object inheriting from a patch becomes a patch itself. The patch target of the parent cannot be changed, so the inheriting object must not define its own target.
SomeObject():
member_name : int = 7
# Patch by definition
SomePatch<SomeObject>():
member_name -= 3
# Patch by inheritance
ChildPatch(SomePatch):
member_name += 4
Applying an inherited patch will also apply its ancestors in descending order, i.e. the highest level ancestor is applied first, while the initiating patch is applied last. If a patch in the chain has multiple parents, they are applied in order of appearence, i.e. in the order they appear in the patch definition.
Namespaces allow the organization of data in a hierarchical way. A namespace can be seen as an address inside a nyan data structure.
Every folder, .nyan
file and nyan object implicitly defines its
namespace by its name and location inside the filesystem. File names
and folder names must not contain a .
(except in the extension
.nyan
) to prevent naming conflicts.
thuglife/units/backstreet.nyan
Data defined in this file is in namespace:
thuglife.units.backstreet
An object in the file is then accessed like this via its FQON:
thuglife.units.backstreet.DrugDealer
Overall, the example defines (at least) 4 namespaces:
thuglife # folder namespace
thuglife.units # folder namespace
thuglife.units.backstreet # file namespace
thuglife.units.backstreet.DrugDealer # object namespace
To reference nyan objects in other files, a namespace they are
in has to be imported using the import
keyword.
import thuglife
Frank(thuglife.units.backstreet.DrugDealer):
speciality = "Meth"
Aliases of namespaces can be defined for convenience with the
import ... (as ...)
statement. An alias must be unique for
every file.
import thuglife.units.backstreet.DrugDealer as Dealer
Frank(Dealer):
speciality = "Meth"
Any intermediate alias also works. The object reference must then be defined as relative to the namespace defined by the alias.
import thuglife.units.backstreet as thugs
Frank(thugs.DrugDealer):
speciality = "Meth"
A user mod that patches loom to increase villager hp by 10 instead of 15.
- Loom is defined in the base data pack
- The mod defines to update the original loom tech
- The tech is researched, which applies the updated loom tech to the villager instance of the current player
# Base game data defines:
Villager(Unit):
name = "Villager"
hp = 25
LoomVillagerHP<Villager>():
hp += 15
# User mod decreases the HP amount:
BalanceHP<LoomVillagerHP>():
hp -= 5
This demonstrates that modding capabilities are strongly limited by the game engine, nyan just assists you in designing a mod api in an intuitive way.
The parents of a nyan::Object
are kind of a mixin for members:
- The child object obtains all the members from its parents
- When a member value is requested, the value is calculated by backtracking through all the parents until the first value definition.
- If name clashes occur, the loading will error, unless you fix them:
- Parent member names can be qualified to fix the ambiguity:
Both Parent
and Other
have a member named member
:
NewObj(Parent, Other):
Parent.member = 1337
Other.member -= 42
Children of that object must access the members with the qualified names as well to make the access clear.
Consider this case, where we have 2 conflicts.
Top():
entry : int = 10
A(Top):
entry += 5
otherentry : int = 0
specialentry : int = 42
B(Top):
entry -= 3
otherentry : int = 1
C():
entry : int = 20
otherentry : int = 2
LOLWhat(A, B, C):
# We now have several conflicts in here!
# How is it resolved?
# A and B both get a member `entry` from Top
# A and B both declare `otherentry` independently
# C declares `entry` and `otherentry` independently
# LOLWhat now inherits from all, so it has
# * `entry` from Top or through A or B
# * `entry` from C
# * `otherentry` from A
# * `otherentry` from B
# * `otherentry` from C
# ->
# to access any of those, the name must be qualified:
A.entry += 1 # or B.entry/Top.entry is the same!
C.entry += 1
A.otherentry += 1
B.otherentry += 1
C.otherentry += 1
specialentry -= 42
OHNoes(LOLWhat):
# access to qualified members remains the same
A.entry += 1
specialentry += 1337
The detection of the qualification requirement works as follows:
- The inheritance list of
LOLWhat
determined byC3
is[A, B, Top, C]
- When in
LOLWhat
theC.entry
value is requested, that list is walked through until a value declaration for each member was found:A
declaresotherentry
andspecialentry
, it changesentry
B
declaresotherentry
and changesentry
- Here, nyan detects that
otherentry
was declared twice - If it was defined without declaration, it errors because no parent
declared
otherentry
- The use of
otherentry
is therefore enforced to be qualified
- Here, nyan detects that
Top
declaresentry
C
declaresentry
andotherentry
- Here, nyan detects that
entry
andotherentry
are declared again - The access to
entry
must hence be qualified, too
- Here, nyan detects that
- nyan concludes that all accesses must be qualified,
except to
specialentry
, as only one declaration was found - The qualification is done by prefixing a
nyan::Object
name which is somewhere up the hierarchy and would grant conflict-free access to that member - That does not mean the value somewhere up the tree is changed! The change is only defined in the current object, the qualification just ensures the correct target member is selected!
If one now has the OHNoes
nyan::Object
and desires to get its values,
the calculation is done like this:
- Just like defining a change, the value must be queried using a distinct name, i. e. the qualification prefix.
- In the engine, you call something like
OHNoes.get("A.entry")
- The inheritance list by C3 of
OHNoes
is[LOLWhat, A, B, Top, C]
- The list is traversed until the declaration of the requested member is found
LOLWhat
does not declare itA
does not declare it either, but we requested"A.entry"
- As the qualified prefix object does not declare it, the prefix is dropped
- The member name is now unique and can be searched for without the prefix further up the tree
B
does not declare theentry
eitherTop
does declare it, now the recursion goes back the other wayTop
defined the value ofentry
to be10
B
wants to subtract3
, soentry
is now7
A
adds5
, soentry
is now12
LOLWhat
adds1
,entry
is now13
OHNoes
adds1
as well, andentry
is returned as14
- The inheritance list by C3 of