diff --git a/README.md b/README.md index eac01dd..cad3a67 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Which is totally possible with **nyan**. Specification ------------- -Read the [specification](doc/nyan.md). +Read the [specification](doc/nyan_specification.md). Current State of the Project diff --git a/doc/README.md b/doc/README.md index 366f383..fb04714 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,4 +1,90 @@ -nyan -==== +# nyan -[language specification](./nyan.md) +## WTF? + +**nyan** is a strongly typed hierarchical key-value database with patch +functionality and inheritance. + + +## Design idea + +[openage](https://github.com/SFTtech/openage) requires a very complex data +storage to represent the hierarchy of its objects. Research and technology +affects numerous units, civilization bonuses, monk conversions and all that +with the goal to be ultimatively moddable by the community: + +Current data representation formats make this nearly impossible to +accomplish. Readability problems or huge lexical overhead led us to +design a language crafted for our needs. + +Enter **nyan**, which is our approach to store data in a new way™. + + +## Core Principles + +* Human-readable language +* More or less compact (readability > memory) +* General-purpose data definition + database features +* Changing data with patches at runtime +* Moddability of defined data +* Portable +* Object-oriented +* Typesafe + + +## Srsly? + +Let's create a new unit with a mod: a japanese tentacle monster. + +``` python +TentacleMonster(Unit): + name = "Splortsch" + hp = 2000 + +Creation(): + creates += {TentacleMonster} + +TentacleMod(Mod): + name = "Add the allmighty tentacle monster to your holy army" + patches = {Creation} +``` + +Things like `Unit` and `Mod` are provided by the game engine, +`TownCenter` is provided by the base game data. + +When the engine activates the mod, your town center can create the new unit. + + +## Why nyan? + +* nyan allows easy modding + * Data packs ship configuration data and game content as `.nyan` files + * Modpacks can change and extend existing information easily, by applying data "patches" + * Patches are applied whenever the `libnyan` user decides when or where to do so +* nyan is typesafe + * The type of a member is stored when declaring it + * The only things nyan can do: Hierarchical data declaration and patches + * No member type casts + * Only allowed operators for a member type can be called +* nyan is invented here™ + * we can change the specification to our needs whenever we want + +## Specification + +A full specification is provided [here](nyan_specification.md). + +## Integration into a Game Engine + +* Some `.nyan` files are shipped with the game engine + * They describe things the engine is capable of, basically the mod API + * That way, the engine can be sure that things exist + * The engine can access all nyan file contents with type safety + * The data files of the game then extend and change the API `nyan::Object`s + +* The nyan database provides a C++ API used by the game engine + * Can parse `.nyan` files and add all information to the database + * Provides hooks so the engine can react on internal changes + +* Data does not contain any executable code but can specify function names + and parameters. The game engine is responsible for calling those + functions or redirecting to custom scripts diff --git a/doc/embedding.md b/doc/embedding.md new file mode 100644 index 0000000..bb898a4 --- /dev/null +++ b/doc/embedding.md @@ -0,0 +1,207 @@ +# Embedding nyan into a Game Engine + +## nyan interpreter + +`.nyan` files are read by the nyan interpreter part of `libnyan`. + +* You feed `.nyan` files into the `nyan::Database` +* All data is loaded and checked for validity +* You can query any member and object of the store +* You can hold `nyan::Object`s as handles +* You can apply patches to any object at a given time, all already-applied patches after that time are undone +* All data history is stored over time + + +## Embedding in the Engine Code + +The mod API definitions in `engine.nyan` have to be designed exacly the way the +C++ engine code is then using it. It sets up the type system so that the nyan +C++ API can then be used to provide the correct information to the program that embeds nyan. + +The load procedure and data access could be done like this: + +1. Load `engine.nyan` +1. Read `pack.nfo` +1. Load `pack.nyan` +1. Apply "mod-activating" patches in `pack.DefaultMod` +1. Let user select one of `engine.StartConfigs.available` +1. Generate a map and place the `CFG.initial_buildings` +1. Display creatable units for each building on that map + +When the newly created villager is selected, it can build towncenters! +And the towncenter can research a healthpoint-upgrade for villagers. + +``` cpp +// callback function for reading nyan files via the engine +// we need this so nyan can access into e.g. archives of the engine. +std::string base_path = "/some/game/root"; +auto file_fetcher = [base_path] (const std::string &filename) { + return std::make_shared(base_path + '/' + filename); +}; + +// initialization of API +auto db = std::make_shared(); +db->load("engine.nyan", file_fetcher); + +// load the userdata +ModInfo nfo = read_mod_file("pack.nfo"); +db->load(nfo.load, file_fetcher); + +// modification view: this is the changed database state +std::shared_ptr root = db->new_view(); + +nyan::Object mod_obj = root->get(nfo.mod); +if (not mod_obj.extends("engine.Mod", 0)) { error(); } + +nyan::OrderedSet mod_patches = mod_obj.get("patches", 0); + +// activation of userdata (at t=0) +nyan::Transaction mod_activation = root->new_transaction(0); + +for (auto &patch : mod_patches.items()) { + mod_activation.add(patch); +} + +if (not mod_activation.commit()) { error("failed transaction"); } + +// presentation of userdata (t=0) +for (auto &obj : root->get("engine.StartConfigs").get("available", 0).items()) { + present_in_selection(obj); +} + +// feedback from ui +nyan::Object selected_startconfig = ...; + +// use result of ui-selection +printf("generate map with config %s", selected_startconfig.get("name", 0)); +place_buildings(selected_startconfig.get("initial_buildings", 0)); + +// set up teams and players +auto player0 = std::make_shared(root); +auto player1 = std::make_shared(root); + + +// ====== let's assume the game runs now +run_game(); + + +// to check if a unit is dead: +engine::Unit engine_unit = ...; +nyan::Object unit_type = engine_unit.get_type(); +int max_hp = unit_type.get("hp", current_game_time); +float damage = engine_unit.current_damage(); +if (damage > max_hp) { + engine_unit.die(); +} +else { + engine_unit.update_hp_bar(max_hp - damage); +} + +// to display what units a selected entity can build: +nyan::Object selected = get_selected_object_type(); +if (selected.extends("engine.Unit", current_game_time)) { + for (auto &unit : selected.get("can_create", current_game_time).items()) { + display_creatable(unit); + } +} + +// technology research: +nyan::Object tech = get_tech_to_research(); +std::shared_ptr &target = target_player(); +nyan::Transaction research = target.new_transaction(current_game_time); +for (auto &patch : tech.get("patches", current_game_time).items()) { + research.add(patch); +} + +if (not research.commit()) { error("failed transaction"); } +``` + + +### Database views + +Problem: Different players and teams have different states of the same nyan tree. + +Solution: Hierarchy of state views. + +A `nyan::View` has a parent which is either the root database or another `nyan::View`. + +The view then stores the state for e.g. a player. + +What does that mean? + +* You can create a view of the main database +* You can create a view of a view +* Querying values respects the view the query is executed in +* If a patch is applied in a view, the data changes are applied in this view + and all children of it. Parent view remain unaffected. + +Querying data works like this: +* `nyan::Object obj = view.get(object_name)` + * The `nyan::Object` is just a handle which is then used for real queries +* `obj.get(member_name, time)` will evaluates the member of the object at a give time + * This returns the `nyan::Value` stored in the member at the given time. + +Patching data works as follows: +* Obtain a patch object from some view + * `nyan::Object patch = view.get(patch_name);` + * If it is known in the view, return it + * Else return it from the parent view +* Create a transaction with this Patch to change the view state at the desired time + * `nyan::Transaction tx = view.new_transaction(time);` +* Add one or more patch objects to the transaction + * `tx.add(patch); tx.add(...);` + * `tx.add(another_patch, view.get(target_object_name))` is used to patch a child of + the patch target. +* Commit the transaction + * `bool success = tx.commit();` + * This triggers, for each patch in the transaction: + * Determine the patch target object name + * If a custom patch target was requested, + check if it was a child of the default patch target at loadtime. + * Copy the patch target object in a (new) state at `time` + * Query the view of the transaction at `time` for the target object, this may recursively query parent views + * If there is no state at `time` in the view of the transaction, create a new state + * Copy the target object into the state at `time` in the view of the transaction + * Linearize the inheritance hierary to a list of patch objects + * e.g. if we have a `SomePatch()` and `AnotherPatch(SomePatch)` and we would like to apply `AnotherPatch`, this will result in `[SomePatch, AnotherPatch]` + * Apply the list left to right and modify the copied target object + * Notify child views that this patch was applied, perform the patch there as well + +This approach allows different views of the database state and integrates with the +patch idea so e.g. team boni and player specific updates can be handled in an "easy" +way. + + +#### API definition example + +openage uses an [ECS-style nyan API](https://github.com/SFTtech/openage/tree/master/doc/nyan/api_reference) for storing game data. + + +### Creating a scripting API + +nyan does provide any possibility to execute code. +But nyan can be used as entry-point for full dynamic scripting APIs: +The names of hook functions to be called are set up through nyan. +The validity of code that is called that way is impossible to check, +so this can lead to runtime crashes. + + +## nyanc - the nyan compiler + +**nyanc** can compile a .nyan file to a .h and .cpp file, this just creates +a new nyan type the same way the primitive types from above are defined. + +Members can then be acessed directly from C++. + +The only problem still unsolved with `nyanc` is: + +If a "non-optimized" `nyan::Object` has multiple parents where some of them +were "optimized" and made into native code by `nyanc`, we can't select +which of the C++ objects to instanciate for it. And we can't create the +combined "optimized" object as the `nyan::Object` appeared at runtime. + +This means we have to provide some kind of annotation, which of the parents +should be the annotated ones. + +Nevertheless, `nyanc` is just an optimization, and has therefore no +priority until we need it. diff --git a/doc/format_version b/doc/format_version new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/doc/format_version @@ -0,0 +1 @@ +2 diff --git a/doc/member_types.md b/doc/member_types.md new file mode 100644 index 0000000..f7bd014 --- /dev/null +++ b/doc/member_types.md @@ -0,0 +1,766 @@ +# nyan Member Types + +This document contains an overview of the available nyan member types and +operations that are allowed on them. + +## Table of Contents + +* [1. Quick Reference](#quick-reference) + * [1.1 Primitive Types](#primitive-types) + * [1.2 Complex Types](#complex-types) + * [1.3 Type Modifiers](#type-modifiers) +* [2. Data Types](#data-types) + * [2.1 int](#int) + * [2.2 float](#float) + * [2.3 bool](#bool) + * [2.4 text](#text) + * [2.5 file](#file) + * [2.6 set](#set) + * [2.7 orderedset](#orderedset) + * [2.8 dict](#dict) + * [2.9 object](#object) +* [3. Type Modifiers](#type-modifiers-1) + * [3.1 abstract](#abstract) + * [3.2 children](#children) + * [3.3 optional](#optional) +* [4. Rules for Operations with Infinity](#rules-for-operations-with-infinity) + +## Quick Reference + +### Primitive Types + +nyan Type | C++ Equivalent | Operators | Description +----------|----------------|-------------------------|----------------------------------- +`int` | `int64_t` | `=`,`+=`,`-=`,`*=`,`/=` | 64-Bit signed integer. +`float` | `double` | `=`,`+=`,`-=`,`*=`,`/=` | double precision floating point value. +`bool` | `bool` | `=`,`&=`,`\|=` | Boolean value. +`text` | `std:string` | `=`,`+=` | String of characters. +`file` | `std:string` | `=` | Path to a file. +`object` | - | `=` | Reference to a nyan object. + + +### Complex Types + +nyan Type | C++ Equivalent | Operators | Description +--------------|----------------------|--------------------------|----------------------------------- +`set` | `std::set` | `=`,`+=`,`-=`,`&=`,`\|=` | Set of primitive values. +`ordered_set` | `std::ordered_set` | `=`,`+=`,`-=`,`&=`,`\|=` | Ordered set of primitive values. +`dict` | `std::unordered_map` | `=`,`+=`,`-=`,`&=`,`\|=` | Key-value pairs of primitive values. + + +### Type Modifiers + +Type Modifier | nyan Types | Description +--------------|------------|----------------------- +`abstract` | `object` | Can assign abstract object references. +`children` | `object` | Only descendants of the object can be assigned, not the object itself. +`optional` | All | The member can be initialized with the placeholder value `None`. + + +## Data Types + +### `int` + +A member with type `int` can store a signed 64-Bit integer value. Positive infinity +(`inf`) and negative infinity (`-inf`) are supported. + +`float` is compatible to `int` which means that `float` values can be used in +operations on integer values. The result of operations with `float` operands is +always floored (i.e. the value is truncated). This behaviour is consistent with +*standard conversion* for integer/float types in C++. + +```python +SomeObject(): + a : int # only declaration + b : int = 5 # declaration and initialization with 5 + c : int = -20 # declaration and initialization with -20 +``` + + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +---------|----------------|---------------------|------------------------------------ +`=` | Assignment | `a = 5` `a = 5.0` | Assigns the operand value to the member. Floats can be used as an operand. +`+=` | Addition | `a += 5` `a += 5.0` | Adds the operand value to the current member value. Floats can be used as an operand. The result is floored. +`-=` | Subtraction | `a -= 5` `a -= 5.0` | Subtracts the operand value from the current member value. Floats can be used as an operand. The result is floored. +`*=` | Multiplication | `a *= 2` `a *= 2.5` | Multiplies the operand value with the current member value. Floats can be used as an operand. The result is floored. +`/=` | Division | `a /= 2` `a /= 2.5` | Divides the current member value by the operand value. Floats can be used as an operand. The result is floored. + +#### Usage Examples + +```python +SomeObject(): + a : int = 10 + b : int = -5 + c : int = 20 + d : int = 1 + +Patch1(): + a = 5 # result: a = 5 (reassignment) + b += 7 # result: b = 2 (addition: -5 + 7) + c -= 5 # result: c = 15 (sutraction: 20 - 5) + d *= 5 # result: d = 5 (multiplication: 1 * 5) + +OtherObject(): + a : int = -7 + b : int = 3 + c : int = 4 + d : int = 8 + +Patch2(): + a -= 5 # result: a = -12 (subtraction: -7 - 5) + b -= -2 # result: b = 5 (addition: 3 - (-2)) + c *= 2.5f # result: c = 10 (multiplication: 4 * 2.5) + d /= 3 # result: d = 2 (division: 8 / 3 = 2.666... floored to 2) +``` + + +### `float` + +A member with type `float` can store a *double precision* floating point value. +Positive infinity (`inf`) and negative infinity (`-inf`) are supported. + +`int` is compatible to `float` which means that `int` values can be used in +operations on integer values. This behaviour is consistent with +*standard conversion* for integer/float types in C++. + +```python +SomeObject(): + a : float # only declaration + b : float = 5.0 # declaration and initialization with 5.0 + c : float = 8.3456 # declaration and initialization with 8.3456 + d : float = -20.2 # declaration and initialization with -20.2 +``` + + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +---------|----------------|---------------------|------------------------------------ +`=` | Assignment | `a = 5.0` `a = 5` | Assigns the operand value to the member. Integers can be used as an operand. +`+=` | Addition | `a += 5.0` `a += 5` | Adds the operand value to the current member value. Integers can be used as an operand. +`-=` | Subtraction | `a -= 5.0` `a -= 5` | Subtracts the operand value from the current member value. Integers can be used as an operand. +`*=` | Multiplication | `a *= 2.5` `a *= 2` | Multiplies the operand value with the current member value. Integers can be used as an operand. +`/=` | Division | `a /= 2.5` `a /= 2` | Divides the current member value by the operand value. Integers can be used as an operand. + + +#### Usage Examples + + +```python +SomeObject(): + a : float = 10.2 + b : float = -5.1 + c : float = 20.6 + d : float = 0.4 + +Patch1(): + a = 5.5 # result: a = 5.5 (reassignment) + b += 1.2 # result: b = 3.9 (addition: -5.1 + 1.2) + c -= 5.0 # result: c = 15.6 (sutraction: 20.6 - 5.0) + d *= 2.0 # result: d = 0.8 (multiplication: 0.4 * 2.0) + +OtherObject(): + a : float = -7.5 + b : float = 3.3 + c : float = 4.0 + d : float = 4.9 + +Patch2(): + a -= 5.0 # result: a = -12.5 (subtraction: -7.5 - 5.0) + b -= -2.0 # result: b = 5.3 (addition: 3.3 - (-2.0)) + c *= 0.5 # result: c = 2.0 (multiplication: 4.0 * 0.5) + d /= 7.0 # result: d = 0.7 (division: 4.9 / 7.0) +``` + + +### `bool` + +A member with type `bool` can store a boolean value (`True` or `False`). + +```python +SomeObject(): + a : bool # only declaration + b : bool = True # declaration and initialization with True + c : bool = False # declaration and initialization with False +``` + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +---------|----------------|---------------|------------------------------------ +`=` | Assignment | `a = True` | Assigns the operand value to the member. +`&=` | Logical AND | `a &= False` | Result is `True` iff both the operand value **and** the current member value are `True`, else `False`. +`\|=` | Logical OR | `a \|= False` | Result is `True` if either the operand value **or** the current member value are `True`, else `False`. + + +#### Usage Examples + + +```python +SomeObject(): + a : bool = True + b : bool = True + c : bool = True + d : bool = False + +Patch(): + a = False # result: a = False (reassignment) + b &= True # result: b = True (True AND True) + c &= False # result: c = False (True AND False) + d |= True # result: d = True (False OR True) +``` + + +### `text` + +A member with type `text` can store an UTF-8 encoded string. + +```python +SomeObject(): + a : text # only declaration + b : text = "This is a string!" # declaration and initialization +``` + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +---------|----------------|--------------|------------------------------------ +`=` | Assignment | `a = "bla"` | Assigns the operand value to the member. +`+=` | Append | `a += "abc"` | Append operand string to end of the current member value. + + +#### Usage Examples + + +```python +SomeObject(): + a : text = "abc" + b : text = "abrakadabra " + +Patch(): + a = "xyz" # result: a = "xyz" (reassignment) + b += "simsalabim" # result: b = "abrakadabra simsalabim" +``` + + +### `file` + +A member with type `text` can store an absolute or relative path to a file +as an UTF-8 encoded string. `/` should be used as a file separator. For +relative path traversal, `.` (current folder) and `..` (parent folder) +can be used. + +```python +SomeObject(): + a : file # only declaration + b : file = "/abs/path/to/file" # declaration and initialization of absolute path + c : file = "../rel/path/to/file" # declaration and initialization of relative path +``` + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +---------|----------------|--------------|------------------------------------ +`=` | Assignment | `a = "bla"` | Assigns the operand value to the member. + + +#### Usage Examples + + +```python +SomeObject(): + a : file = "path/to/file" + +Patch(): + a = "new/path/to/file" # result: a = "new/path/to/file" (reassignment) +``` + + +### `object` + +A member with type `object` can store a nyan object reference. +This reference must not be abstract (i.e. all members have +a value defined). Furthermore, it must be type-compatible to the +defined type at load-time. + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +SomeObject(): + a : OtherObject # only declaration + b : OtherObject = OtherObject # declaration and initialization with OtherObject + c : OtherObject = ChildObject # declaration and initialization with ChildObject +``` + + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +-----------|----------------|-----------------|------------------------------------ +`=` | Assignment | `a = Object` | Assigns the operand value to the member. + + +#### Usage Examples + + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +SomeObject(): + a : OtherObject = OtherObject + +Patch(): + a = ChildObject # result: a = ChildObject (reassignment) +``` + + +### `set` + +A member with type `set` can store a collection of items with a predefined +type. The allowed item type must be specified during the member declaration. +Sets cannot contain duplicates of an item. They can be empty. + +If the set type is `object`, references must not be abstract (i.e. all +members have a value defined). Furthermore, they must be type-compatible to +the set type. + +`set` does not preserve the input order of items. + + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +DifferentChildObject(OtherObject): + pass + +SomeObject(): + a : set(OtherObject) # only declaration + a : set(OtherObject) = {} # declaration and initialization with empty set + b : set(OtherObject) = {ChildObject} # declaration and initialization with {ChildObject} + c : set(OtherObject) = {OtherObject, # declaration and initialization with multiple items + ChildObject, + DifferentChildObject} +``` + + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +------------|----------------|---------------------------------------|------------------------------------ +`=` | Assignment | `a = {}` | Assigns the operand value to the member. +`+=`, `\|=` | Union | `a += {}`
`a \|= {}` | Performs a union of the operand and the current member value. Items from the operand are added to the member set. +`-=` | Difference | `a -= {}` | Calculates the difference of the operand and the current member value. Items from the operand are removed from the member set. +`&=` | Intersection | `a &= {}` | Performs an intersection of the operand and the current member value. Items that are not in both the operand and the member set are removed from the member set. + + +#### Usage Examples + + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +DifferentChildObject(OtherObject): + pass + +SomeObject(): + a : set(OtherObject) = {} + b : set(OtherObject) = {OtherObject} + c : set(OtherObject) = {ChildObject} + d : set(OtherObject) = {ChildObject, DifferentChildObject} + +Patch(): + a = {DifferentChildObject} # result: a = {DifferentChildObject} (reassignment) + b += {DifferentChildObject} # result: b = {OtherObject, DifferentChildObject} (union) + c -= {ChildObject} # result: c = {} (difference) + d &= {ChildObject, OtherObject} # result: d = {ChildObject} (intersection) +``` + + +### `orderedset` + +A member with type `orderedset` can store a collection of items with a predefined +type. The allowed item type must be specified during the member declaration. +Sets cannot contain duplicates of an item. They can be empty. + +If the set type is `object`, references must not be abstract (i.e. all +members have a value defined). Furthermore, they must be type-compatible to +the set type. + +`orderedset` does preserve the input order of items. + + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +DifferentChildObject(OtherObject): + pass + +SomeObject(): + a : orderedset(OtherObject) # only declaration + a : orderedset(OtherObject) = o{} # declaration and initialization with empty set + b : orderedset(OtherObject) = o{ChildObject} # declaration and initialization with o{ChildObject} + c : orderedset(OtherObject) = o{OtherObject, # declaration and initialization with multiple items + ChildObject, + DifferentChildObject} +``` + + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +------------|----------------|-----------------------------------------|------------------------------------ +`=` | Assignment | `a = o{}` | Assigns the operand value to the member. +`+=`, `\|=` | Union | `a += o{}`
`a \|= o{}` | Performs a union of the operand value and the current member value. Items from the operand are added to the member set. +`-=` | Difference | `a -= o{}`; `a -= {}` | Calculates the difference of the operand value and the current member value. Items from the operand are removed from the member set. +`&=` | Intersection | `a &= o{}`; `a &= {}` | Performs an intersection of the operand value and the current member value. Items that are not in both the operand and the member set are removed from the member set. + + +#### Usage Examples + + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +DifferentChildObject(OtherObject): + pass + +SomeObject(): + a : orderedsetset(OtherObject) = o{} + b : orderedsetset(OtherObject) = o{OtherObject} + c : orderedsetset(OtherObject) = o{ChildObject} + d : orderedsetset(OtherObject) = o{ChildObject, DifferentChildObject} + +Patch(): + a = o{DifferentChildObject} # result: a = o{DifferentChildObject} (reassignment) + b += o{DifferentChildObject} # result: b = o{OtherObject, DifferentChildObject} (union) + c -= o{ChildObject} # result: c = o{} (difference) + d &= o{ChildObject, OtherObject} # result: d = o{ChildObject} (intersection) +``` + + +### `dict` + +A member with type `dict` can store a collection of key-value pairs. +Both the key type and value type must be specified during the member declaration. +Dicts cannot contain items with duplicate keys. They can be empty. + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +DifferentChildObject(OtherObject): + pass + +SomeObject(): + a : dict(OtherObject, int) # only declaration + b : dict(OtherObject, int) = {} # declaration and initialization with empty dict + c : dict(OtherObject, int) = {ChildObject: 2} # declaration and initialization with single item + d : dict(OtherObject, int) = {OtherObject: 3, # declaration and initialization with multiple items + ChildObject: -15, + DifferentChildObject: inf} +``` + +#### Operators + +The following operators can be used when inheriting from or patching a nyan object. + +Operator | Operation | Examples | Description +------------|------------------|--------------------------------------|------------------------------------ +`=` | Assignment | `a = {key: val}` | Assigns the operand value to the member. +`+=`, `\|=` | Union | `a += {key: val}` `a \|= {key: val}` | Performs a union of the operand value and the current member value. Items from the operand are added to the member dict. +`-=` | Difference | `a -= {key}` | Calculates the difference of the operand value and the current member value. Items using a key from the operand set are removed from the member dict. +`&=` | Intersection | `a &= {key: val}` `a &= {key}` | Performs an intersection of the operand value and the current member value. When given a set, only items that have a key specified in the set are kept. When given a dict, only items (i.e. key **and** value) that are in both dicts are kept.´ + + +#### Usage Examples + + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +DifferentChildObject(OtherObject): + pass + +SomeObject(): + a : dict(OtherObject, int) = {} + b : dict(OtherObject, int) = {} + c : dict(OtherObject, int) = {ChildObject: 2} + d : dict(OtherObject, int) = {ChildObject: 5, + DifferentChildObject: -10} + +Patch(): + a = {OtherObject: 50} # result: a = {OtherObject: 50} (reassignment) + b += {ChildObject: 5} # result: c = {ChildObject: 5} (union) + c -= {ChildObject} # result: d = {} (difference) + d &= {ChildObject, OtherObject} # result: e = {ChildObject: 5} (intersection) +``` + + +## Type Modifiers + + +### `abstract` + +`abstract` is a type modifier for the `object` data type. It signifies +that references to abstract nyan objects can be assigned as member values. + +```python +OtherObject(): + # is abstract because member 'x' is not initialized + x : int + +ChildObject(OtherObject): + # not abstract because member 'x' is initialized with 5 + x = 5 + +SomeObject(): + a : abstract(OtherObject) # declaration of a to allow abstract objects with type OtherObject + b : abstract(OtherObject) = OtherObject # declaration of b to allow abstract objects with type OtherObject + # and initialization with OtherObject + c : abstract(OtherObject) = ChildObject # declaration of b to allow abstract objects with type OtherObject + # and initialization with ChildObject + d : OtherObject = ChildObject # ALLOWED (ChildObject is non-abstract) + e : OtherObject = OtherObject # NOT ALLOWED (OtherObject is abstract) +``` + +#### Usage Examples + + +```python +OtherObject(): + x : int + +ChildObject(OtherObject): + x = 5 + +SomeObject(): + a : abstract(OtherObject) = OtherObject + b : abstract(OtherObject) = ChildObject + +Patch(): + a = ChildObject # result: a = ChildObject (reassignment) + b = ChildObject # result: b = ChildObject (no effect) +``` + + +### `children` + +`children` is a type modifier for the `object` data type. It signifies +that only references to the descandents of a nyan object can be assigned +as member values. References to the object with the same identity are not +allowed. + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +SomeObject(): + a : children(OtherObject) # declaration of a to allow only descendants of OtherObject + b : children(OtherObject) = ChildObject # declaration of b to allow only descendants of OtherObject + # and initialization with ChildObject + c : children(OtherObject) = OtherObject # NOT ALLOWED +``` + +#### Usage Examples + + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +DifferentChildObject(OtherObject): + pass + +SomeObject(): + a : children(OtherObject) = DifferentChildObject + b : children(OtherObject) = ChildObject + c : children(OtherObject) = ChildObject + +Patch(): + a = ChildObject # result: a = ChildObject (reassignment) + b = DifferentChildObject # result: b = DifferentChildObject (no effect) + c = OtherObject # NOT ALLOWED +``` + + +### `optional` + +A member with type modifier `optional` can have the placeholder value +`None` assigned. `None` indicates that the member is initialized, but +has no regular value. `None` can only be assigned and not used as a +second operand in other operations. Additionally, operations other than +assignment have no effect on members that have `None` assigned. + +Members of any data type can have the `optional` modifier. `optional` +members still need to be initialized with either `None` or a regular +value to not be abstract. + +```python +SomeObject(): + a : optional(int) # declaration of an int as optional + b : optional(float) # declaration of a float as optional + c : optional(set(OtherObject)) # declaration of a set(OtherObject) as optional + d : optional(OtherObject) # declaration of a nyan object reference to OtherObject as optional + +OtherObject(): + a : optional(int) = None # declaration and initialization with None + b : optional(OtherObject) = OtherObject # declaration and initialization with regular value +``` + +#### Usage Examples + + +```python +OtherObject(): + pass + +ChildObject(OtherObject): + pass + +SomeObject(): + a : optional(int) = 5 + b : optional(float) = None + c : optional(set(OtherObject)) = None + d : optional(OtherObject) = OtherObject + +Patch(): + a = None # result: a = None (reassignment) + b += 10.0 # result: b = None (no effect) + c = {} # result: c = {} (reassignment) + d = ChildObject # result: d = ChildObject (reassignment) +``` + + +## Rules for Operations with Infinity + +Members of type `int` or `float` support the assignment of positive infinity (`inf`) +and negative infinity (`-inf`). Infinity should not be seen as a regular value. +Instead, `inf` is *interpreted* as a value that is higher (lower for `-inf`) +than the maximum possible integer/float value. nyan will handle operations involving +`inf` that are not assignments according to the standard rules of calculus (see +Examples section). + +Operations that would lead to nyan calculating + +* `inf - inf` +* `inf / inf` +* `inf * 0` + +will throw an error, since the result is undefined. This situation can generally +be avoided by not using `inf` as a second operand for any operation other than +assignment. + + +### Examples + +Using infinity as the first operand and a regular value as the second operand: + +```python +SomeObject(): + a : int = inf + b : int = inf + c : int = inf + d : int = inf + e : int = inf + +Patch1(): + a += 5 # result: a = inf + b -= 5 # result: b = inf + c *= 5 # result: c = inf + d *= -5 # result: d = -inf + e *= 0 # NOT ALLOWED + f /= 5 # result: f = inf +``` + +Using a regular value as the first operand and infinity as the second operand: + +```python +SomeObject(): + a : int = 5 + b : int = 5 + c : int = 5 + d : int = 5 + e : int = 5 + +Patch2(): + a += inf # result: a = inf + b -= inf # result: b = -inf + c *= inf # result: c = inf + d *= -inf # result: d = -inf + e /= inf # result: e = 0 +``` + +Using infinity as the first and second operand: + +```python +SomeObject(): + a : int = inf + b : int = inf + c : int = inf + d : int = inf + e : int = inf + f : int = inf + +Patch3(): + a += inf # result: a = inf + b *= inf # result: b = inf + c *= -inf # result: c = -inf + d -= inf # NOT ALLOWED + e += -inf # NOT ALLOWED + f /= inf # NOT ALLOWED +``` diff --git a/doc/nyan.md b/doc/nyan.md deleted file mode 100644 index e62b1d1..0000000 --- a/doc/nyan.md +++ /dev/null @@ -1,1169 +0,0 @@ -nyan ----- - -## WTF? - -**nyan** is a strongly typed hierarchical key-value database with patch -functionality and inheritance. - - -## Srsly? - -Let's create a new unit with a mod: a japanese tentacle monster. - -``` python -TentacleMonster(Unit): - name = "Splortsch" - hp = 2000 - -Creation(): - creates += {TentacleMonster} - -TentacleMod(Mod): - name = "Add the allmighty tentacle monster to your holy army" - patches = {Creation} -``` - -Things like `Unit` and `Mod` are provided by the game engine, -`TownCenter` is provided by the base game data. - -When the engine activates the mod, your town center can create the new unit. - - - -## Design idea - -[openage](https://github.com/SFTtech/openage) requires a very complex data -storage to represent the hierarchy of its objects. Research and technology -affects numerous units, civilization bonuses, monk conversions and all that -with the goal to be ultimatively moddable by the community: - -Current data representation formats make this nearly impossible to -accomplish. Readability problems or huge lexical overhead led us to -design a language crafted for our needs. - -Enter **nyan**, which is our approach to store data in a new way™. - - -## Design goals - -Requirements: - -* nyan remains a general-purpose data language -* Data is stored in `.nyan` files -* Human readable -* Portable -* More or less compact (readability > memory) -* Data is stored as members of `nyan::Object`s -* Data is changed by patches that change members of `nyan::Object`s -* Patches can be changed by patches, that way, any mod can be created -* Data does not contain any executed code but can specify function names - and parameters. The game engine is responsible for calling those - functions or redirecting to custom scripts -* Namespaces to create a logical hierarchy of `nyan::Object`s -* Some `.nyan` files are shipped with the game engine - * They describe things the engine is capable of, basically the mod api - * That way, the engine can be sure that things exist - * The engine can access all nyan file contents with type safety - * The data files of the game then extend and change the API `nyan::Object`s -* The nyan database provides a C++ API used by the game engine - * Can parse `.nyan` files and add all information to the database - * Provides hooks so the engine can react on internal changes - - -## Language features - -* nyan allows easy modding - * Data packs ship configuration data and game content as .nyan files - * Mod Packs can change and extend existing information easily, - by applying data "patches" - * Patches are applied whenever the `libnyan` user decides - when or where to do so -* nyan is typesafe - * The type of a member is stored when declaring it - * No member type casts - * Only allowed operators for a member type can be called -* nyan is invented here™ - * we can change the specification to our needs whenever we want - -Concept: - -* The only things nyan can do: Hierarchical data declaration and patches - * *`nyan::Object`*: In a .nyan file, you write down `nyan::Object`s - * A `nyan::Object` has an aribitrary number of members - * A member has a data type like `int` -* `nyan::Object`s support a hierarchy by inheritance - * You can fetch values from a `nyan::Object` and the result is determined - by walking up the whole inheritance tree - * This allows changing a value in a parent class and all childs are - affected then -* `nyan::Object`s are placed in namespaces to organize the directory structure - - -### Data handling - -* *`nyan::Object`*: versatile atomic base type - * Has named members which have a type and maybe a value - * `nyan::Object`s remain abstract until all members have values - * There exists no order of members -* **nyan::Patch**: is a `nyan::Object` and denominates a patch - * Patches are used to change a target `nyan::Object` at runtime - * It is created for exactly one `nyan::Object` with `PatchName` - * Can modify **member values** of the target `nyan::Object` - * Can add **inheritance** parents of the target `nyan::Object` - * Can *not* add new members or remove them - * When activated, member values are calculated by inheritance - * The patch inherits from the target object - * Values are calculated top-down - * The resulting values are stored as the target object -* A `nyan::Object` can inherit from an ordered set of `nyan::Object`s - (-> from a *nyan::Patch* as well) - * Members of parent objects are inherited - * When inheriting, existing values can be modified by operators - defined for the member type - * Member values are calculated accross the inheritance upwards - * That way, patching a parent object impacts all children - * When a value from a `nyan::Object` is retrieved, - walk up every time and sum up the value - * If there is a member name clash, there can be two reasons for it - * The member originates from a common base object (aka the [diamond problem](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem)) - * We use [C3 linearization](https://en.wikipedia.org/wiki/C3_linearization) to determine the calculation order - * Just access the member as always (`member += whatever`) - * Two independent objects define the same member - and you inherit from both - * The child class must access the members by `ParentObj.member` - * Further child objects must use the same explicit access - * If both conflicts occur simultaneously (common parent defines member - and another parent defines it independently) - * C3 is applied first (unifies members by a common parent) - * Name conflicts must then resolved by manual qualification again - - -### Syntax - -``` python -# 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[+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 = value - ... - - some_member : Inherited = NestedObject - ... -``` - -* An object declares a named storage space with then has key-value pairs -* If an object does not have a parent, it implicitly inherits from the built-in `Object` -* Nested object names are prefixed the parent name, on top level their name is `ParentObject.NestedObject` -* A member is created by *declaring* it by `member_name : type` -* A member is *defined* by `member_name = value` -* An inherited member is referenced by either `member_name` or `ParentObject.member_name` -* The declaration and definition can be combined: - `member_name : type = value` -* A member can never be defined if it was not declared -* A `nyan::Object` is "abstract" iff it contains at least one undefined member -* A `nyan::Object` member **type** can never be changed once declared - -* It is a patch iff `` is written in the definition - * The patch will be applied for the specified object only - * A patch can add a new inheritance parent at the front of the parent list - * Done with the `[+AdditionalParent, AnotherParent+, ...]` syntax - * `+Parent` adds parent to the end, `Parent+` to the front - * Reason: the direction of the `+` indicates the existing list - * If target has parents `[A, B]` and we apply `[+C, +D, E+, B+]`, the result is `[E, A, B, C, D]` - * The activation of this parent must not induce name clashes of members, [see below](#Multi inheritance). When the patch is loaded, this is checked. - * This can be used to inject a "middle object" in between two inheriting - objects, because the multi inheritance linearization resolves the order - * Imagine something like `TentacleMonster -> Unit` - * What we now want is `TentacleMonster -> MonsterBase -> Unit` - * What we do first is create `MonsterBase -> Unit` - * After applying a patch with `+MonsterBase` it is `TentacleMonster -> MonsterBase, Unit` - * The linearization will result in `TentacleMonster -> MonsterBase -> Unit` - * A patch modifies the value of the target object only - * The target object operator will remain the same - * The target object value will be changed according to the operation - in the patch - * A patch replaces the operator and value of a target object, - if the patch operation is prefixed with an `@` - * Multiple `@` characters are allowed, so they are transferred - into the patched object. This is only allowed for patching patches. - * When applied, the `n` `@` chars from the patch will result - in `n-1` `@` chars in the patched patch. That way, - operator overrides can be propagated to arbitrarily nested patches. - * The patch will fail to be loaded if: - * The patch target is not known - * Any of changed members is not known in the patch target - * Any of the added parents is not known - * -> Blind patching is not allowed - * The patch will succeed to load if: - * All members of the patch are available in the patch target - * The patch target already inherits from a parent to be added - * -> Inheritance patching doesn't conflict with other patches - - -#### Multi inheritance - -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`: - -``` python -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. - -``` python -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 by `C3` is `[A, B, Top, C]` -* When in `LOLWhat` the `C.entry` value is requested, that list is walked - through until a value declaration for each member was found: - * `A` declares `otherentry` and `specialentry`, it changes `entry` - * `B` declares `otherentry` and changes `entry` - * 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 - * `Top` declares `entry` - * `C` declares `entry` and `otherentry` - * Here, nyan detects that `entry` and `otherentry` are declared again - * The access to `entry` must hence be qualified, too -* nyan concludes that all accesses must be qualified, - except to `specialentry`, as only one declaration was found -* The qualification is done by prefixing the precedes 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 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 gone through until the declaration of the requested member - was found - * `LOLWhat` did not declare it - * `A` did 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 the `entry` either - * `Top` does declare it, now the recursion goes back the other way - * `Top` defined the value of `entry` to `10` - * `B` wants to subtract `3`, so `entry` is `7` - * `A` adds `5`, so `entry` is `12` - * `LOLWhat` adds `1`, `entry` is `13` - * `OHNoes` adds `1` as well, and `entry` is returned to be `14` - - -#### Types - -* Members of `nyan::Object` must have a type, which can be a - * primitive type - - `text`: `"lol"` - (duh.) - - `int`: `1337` - (some number) - - `float`: `42.235`, `inf`, `-inf` - (some floating point number) - - `bool`: `true`, `false` - (some boolean value) - - `file`: `"./name" ` - (some filename, relative to the directory the defining nyan file is located at. - If absolute, the path is relative to an engine defined root directory.) - * ordered set of elements of a type: `orderedset(type)` - * set of elements of a type: `set(type)` - * dictionary of elements of a type to elements of another type: `dict(keyType, valueType)` - * currently, there is **no** `list(type)` specified, - but may be added later if needed - * `nyan::Object`, to allow arbitrary hierarchies - -* Type hierarchy - * A `nyan::Object`'s type name equals its name: `A()` has type `A` - * A `nyan::Object` `isinstance` of its type and all the types of its parent `nyan::Object`s - * Sounds complicated, but is totally easy: - * If an object `B` inherits from an object `A`, it also has the type `A` - * Just like the multi inheritance of other programming languages - * Again, name clashes of members must be resolved - to avoid the diamond problem - -* All members support the assignment operator `=` -* Many other operators are defined on the primitive types - * `text`: `=`, `+=` - * `int` and `float`: `=`, `+=`, `*=`, `-=`, `/=` - * `bool`: `=`, `&=`, `|=` - * `file`:`= "./delicious_cake.png"` - * `set(type)`: - * assignment: `= {value, value, ...}` - * union: ` += {..}`, `|= {..}` -> add values to set - * subtract: `-= {..}` -> remove those values - * intersection: `&= {..}` -> keep only values element of both - * `orderedset(type)`: - * assignment: `= o{value, value, ...}` - * append: `+= o{..}` -> add values to the end - * subtract: `-= o{..}`, `-= {..}` -> remove those values - * intersection: `&= o{..}`, `&= {..}` -> keep only values element of both - * TODO: `dict(keytype, valuetype)`: - * assignment: `= {key: value, k: v, ...}` - * insertion of data: `+= {k: v, ...}`, `|= {..}` - * deletion of keys: `-= {k, k, ...}`, `-= {k: v, ..}` - * keep only those keys: `&= {k, k, ..}`, `&= {k: v, ..}` - * `nyan::Object` reference: - * `= NewObject` set the reference to some other object. - This reference must not be non-abstract (i.e. all members - have a value defined). And it must be type-compatible, of course. - - - -### Namespaces, imports and forward declarations - -Namespaces and imports work pretty much the same way as Python defined it. -They allow to organize data in an easy hierarchical way on your file system. - - -#### Implicit namespace - -A nyan file name is implies its namespace. That means the file name must -not contain a `.` (except the `.nyan`) to prevent naming conflicts. - -`thuglife/units/backstreet.nyan` - -Data defined in this file is in namespace: - -`thuglife.units.backstreet` - -An object is then accessed like: - -`thuglife.units.backstreet.DrugDealer` - - -#### Importing - -A file is loaded when another file imports it. -This is done by loading another namespace. - -``` python -import thuglife -``` - -You can define convenience aliases of fully qualified names -with the `import ... (as ...)` statement. - -This imports the namespace with an alias (right side) -which expands to the left side when used. - -``` python - -Frank(thuglife.units.backstreet.DrugDealer): - speciality = "Meth" - - -# is the same as: - -import thuglife.units.backstreet.DrugDealer as DrugDealer - -Frank(DrugDealer): - speciality = "Meth" - - -# which is also the same as - -import thuglife.units.backstreet as thugs - -Frank(thugs.DrugDealer): - speciality = "Meth" -``` - - -#### Cyclic dependencies - -Inheritance can never be cyclic (duh). Member value usage can be cyclic. - -Usage as member value can be done even though the object is not yet -declared. This works as objects in member values are always "pointers". - -The compatibility for the value type is tested just when the -referenced object was actually loaded. -This means there are implicit forward declarations. - -Example: deer death - -``` python -# engine features: - -Ability(): - ... - -Behaviour(): - ... - -Resource(): - name : text - -DieAbility(Ability): - die_animation : file - become : Unit - -Huntable(Ability): - hunting_reaction : Behaviour - -Unit(): - abilities : set(Ability) - hp : int - graphic : file - -ResourceAmount(): - type : Resource - amount : float - -ResourceSpot(): - resources : set(ResourceAmount) - -IntelligentFlee(Behaviour): - ... - - -# content pack: - -Animal(Unit): - ... - -Deer(Animal): - DeerDie(DieAbility): - die_animation = "deer_die.png" - become = DeadDeer - - DeerHuntable(Huntable): - hunting_reaction = IntelligentFlee - - hp = 10 - graphic = "deer.png" - abilities |= {DeerDie, DeerHuntable} - -DeadDeer(Deer, ResourceSpot): - DeerFood(ResourceAmount): - type = Food - amount = 250 - - graphic = "dead_deer.png" - resources = {DeerFood} -``` - - -##### Forward declarations - -The engine has to invoke the check whether all objects that were used as -forward declaration were actually defined. - -If there are dangling forward declaration objects when invoking that -consistency check, a list of missing objects will be provided. These have -to be provided in order for nyan to load. Otherwise the objects affected -by incomplete members cannot be used. - - -##### Cyclic avoidance - -If you encounter a cyclic dependency, try to redesign your data model by -extracting the common part as a separate object and then use it in both -old ones. - - -## nyan interpreter - -`.nyan` files are read by the nyan interpreter part of `libnyan`. - -* You feed `.nyan` files into the `nyan::Database` -* All data is loaded and checked for validity -* You can query any member and object of the store -* You can hold `nyan::Object`s as handles -* You can apply patches to any object at a given time, all already-applied patches after that time are undone -* All data history is stored over time - - -### Database views - -Problem: Different players and teams have different states of the same nyan tree. - -Solution: Hierarchy of state views. - -A `nyan::View` has a parent which is either the root database or another `nyan::View`. - -The view then stores the state for e.g. a player. - -What does that mean? - -* You can create a view of the main database -* You can create a view of a view -* Querying values respects the view the query is executed in -* If a patch is applied in a view, the data changes are applied in this view - and all children of it. Parent view remain unaffected. - -Querying data works like this: -* `nyan::Object obj = view.get(object_name)` - * The `nyan::Object` is just a handle which is then used for real queries -* `obj.get(member_name, time)` will evaluates the member of the object at a give time - * This returns the `nyan::Value` stored in the member at the given time. - -Patching data works as follows: -* Obtain a patch object from some view - * `nyan::Object patch = view.get(patch_name);` - * If it is known in the view, return it - * Else return it from the parent view -* Create a transaction with this Patch to change the view state at the desired time - * `nyan::Transaction tx = view.new_transaction(time);` -* Add one or more patch objects to the transaction - * `tx.add(patch); tx.add(...);` - * `tx.add(another_patch, view.get(target_object_name))` is used to patch a child of - the patch target. -* Commit the transaction - * `bool success = tx.commit();` - * This triggers, for each patch in the transaction: - * Determine the patch target object name - * If a custom patch target was requested, - check if it was a child of the default patch target at loadtime. - * Copy the patch target object in a (new) state at `time` - * Query the view of the transaction at `time` for the target object, this may recursively query parent views - * If there is no state at `time` in the view of the transaction, create a new state - * Copy the target object into the state at `time` in the view of the transaction - * Linearize the inheritance hierary to a list of patch objects - * e.g. if we have a `SomePatch()` and `AnotherPatch(SomePatch)` and we would like to apply `AnotherPatch`, this will result in `[SomePatch, AnotherPatch]` - * Apply the list left to right and modify the copied target object - * Notify child views that this patch was applied, perform the patch there as well - -This approach allows different views of the database state and integrates with the -patch idea so e.g. team boni and player specific updates can be handled in an "easy" -way. - - - -### Embedding nyan - -A mod API could be implemented as follows: Create a `nyan::Object` named `Mod` -that has a member with a set of patches to apply. To add new data to the engine, -inherit from this `Mod`-object and add patches to the set. This `Mod`-object is -registered to the engine with a mod description file. - -#### API definition example - -In practice, this could look like this: - -``` python -# Engine API definition: engine.nyan - -Mod(): - patches : orderedset(Patch) - -Tech(): - patches : orderedset(Patch) - -Unit(): - hp : int - can_create : set(Unit) = {} - can_research : set(Tech) = {} - -CFG(): - initial_buildings : set(Unit) - name : text - -StartConfigs(): - # available start game configurations - available : set(CFG) = {} -``` - -``` python -# Data pack: pack.nyan - -import engine - -Villager(engine.Unit): - hp = 100 - can_create = {TownCenter} - -Loom(Tech): - HPBoost(): - hp += 50 - - patches = o{HPBoost} - -TownCenter(engine.Unit): - hp = 1500 - can_create = {Villager} - can_research = {Loom} - -DefaultConfig(engine.CFG): - initial_buildings = {TownCenter} - name = "you'll start with a town center" - -DefaultMod(engine.Mod): - Activate(): - available += {DefaultConfig} - - patches = o{Activate} -``` - -Mod information file `pack.nfo`: -``` ini -load: pack.nyan -mod: pack.DefaultMod -# could be extended with dependency and version information -``` - -#### Embedding in the engine - -The mod API definitions in `engine.nyan` have to be designed exacly the way the -C++ engine code is then using it. It sets up the type system so that the nyan -C++ API can then be used to provide the correct information to the program that embeds nyan. - -The load procedure and data access could be done like this: - -1. Load `engine.nyan` -1. Read `pack.nfo` -1. Load `pack.nyan` -1. Apply "mod-activating" patches in `pack.DefaultMod` -1. Let user select one of `engine.StartConfigs.available` -1. Generate a map and place the `CFG.initial_buildings` -1. Display creatable units for each building on that map - -When the newly created villager is selected, it can build towncenters! -And the towncenter can research a healthpoint-upgrade for villagers. - -``` cpp -// callback function for reading nyan files via the engine -// we need this so nyan can access into e.g. archives of the engine. -std::string base_path = "/some/game/root"; -auto file_fetcher = [base_path] (const std::string &filename) { - return std::make_shared(base_path + '/' + filename); -}; - -// initialization of API -auto db = std::make_shared(); -db->load("engine.nyan", file_fetcher); - -// load the userdata -ModInfo nfo = read_mod_file("pack.nfo"); -db->load(nfo.load, file_fetcher); - -// modification view: this is the changed database state -std::shared_ptr root = db->new_view(); - -nyan::Object mod_obj = root->get(nfo.mod); -if (not mod_obj.extends("engine.Mod", 0)) { error(); } - -nyan::OrderedSet mod_patches = mod_obj.get("patches", 0); - -// activation of userdata (at t=0) -nyan::Transaction mod_activation = root->new_transaction(0); - -for (auto &patch : mod_patches.items()) { - mod_activation.add(patch); -} - -if (not mod_activation.commit()) { error("failed transaction"); } - -// presentation of userdata (t=0) -for (auto &obj : root->get("engine.StartConfigs").get("available", 0).items()) { - present_in_selection(obj); -} - -// feedback from ui -nyan::Object selected_startconfig = ...; - -// use result of ui-selection -printf("generate map with config %s", selected_startconfig.get("name", 0)); -place_buildings(selected_startconfig.get("initial_buildings", 0)); - -// set up teams and players -auto player0 = std::make_shared(root); -auto player1 = std::make_shared(root); - - -// ====== let's assume the game runs now -run_game(); - - -// to check if a unit is dead: -engine::Unit engine_unit = ...; -nyan::Object unit_type = engine_unit.get_type(); -int max_hp = unit_type.get("hp", current_game_time); -float damage = engine_unit.current_damage(); -if (damage > max_hp) { - engine_unit.die(); -} -else { - engine_unit.update_hp_bar(max_hp - damage); -} - -// to display what units a selected entity can build: -nyan::Object selected = get_selected_object_type(); -if (selected.extends("engine.Unit", current_game_time)) { - for (auto &unit : selected.get("can_create", current_game_time).items()) { - display_creatable(unit); - } -} - -// technology research: -nyan::Object tech = get_tech_to_research(); -std::shared_ptr &target = target_player(); -nyan::Transaction research = target.new_transaction(current_game_time); -for (auto &patch : tech.get("patches", current_game_time).items()) { - research.add(patch); -} - -if (not research.commit()) { error("failed transaction"); } -``` - - -### Creating a scripting API - -nyan does provide any possibility to execute code. -But nyan can be used as entry-point for full dynamic scripting APIs: -The names of hook functions to be called are set up through nyan. -The validity of code that is called that way is impossible to check, -so this can lead to runtime crashes. - - -## nyanc - the nyan compiler - -**nyanc** can compile a .nyan file to a .h and .cpp file, this just creates -a new nyan type the same way the primitive types from above are defined. - -Members can then be acessed directly from C++. - -The only problem still unsolved with `nyanc` is: - -If a "non-optimized" `nyan::Object` has multiple parents where some of them -were "optimized" and made into native code by `nyanc`, we can't select -which of the C++ objects to instanciate for it. And we can't create the -combined "optimized" object as the `nyan::Object` appeared at runtime. - -This means we have to provide some kind of annotation, which of the parents -should be the annotated ones. - -Nevertheless, `nyanc` is just an optimization, and has therefore no -priority until we need it. - - -## openage specific "standard library" - -nyan in openage has specific requirements how to handle patches: -mods, technologies, technology-technologies. - -### Defined `nyan::Object`s - -The openage engine defines a few objects to inherit from. -The engine reacts differently when children of those `nyan::Object`s are -created. - -#### Data updates - -##### `Mod`: Game mods - -* It has a member `patches` where you should add your patches. -* When created, the Engine will apply the patches on load time. - -##### `Tech`: Technologies - -* Has a member `updates` that contains the patches to apply when researched - - -#### Engine features - -A game engine can only process and display things it was programmed for. -That's why those features have explicit hooks when used in nyan. - -The nyan definition of objects that provide configuration of such features -is thereby shipped with the engine. - -A few examples - -##### `Resource`: Resource types - -* The engine supports adding and removing new resources via mods -* The GUI, statistics, game logic, ... subsystems dynamically take - care of the available resources - -##### `Ability`: Available unit actions - -* Base object for something a unit can do -* `Movement`, `Gather`, `ResourceGenerator`, - `ResourceSpot`, ... defined and implemented by engine as well -* The engine implements all kinds of things for the abilities - and also triggers actions when the ability is invoked - -##### `DropSite`: Object where resources can be brought to - -* The engine movement and pathfinding system must know about dropsites -* Configures the allowed resources - -##### `Unit`: In-game objects - -* Base object for things you can see in-game -* Provides `ability` member which contains a set of abilities - -##### Many many more. - -* Your game engine may define completely different objects -* How and when a patch is applied is completely up to the engine -* nyan is just the tool for keeping the data store - - -### Unit hierarchy - -By using the objects defined by the engine, units can be defined in a -nyan file not part of the engine, but rather a data pack for it. - -Lets start with an example inheritance hierarchy: - -`malte23 (instance) <- Crossbowman <- Archer <- RangedUnit (engine) <- Unit (engine) <- Object (built in)` - -Why: - -* There's a base nyan object, defined in the language internally -* The engine support units that move on screen -* The engine supports attack projectile ballistics -* All archers may receive armor/attack bonus updates -* Crossbowmen is an archer and can be built at the archery - -`malte23` walks on your screen and dies laughing. -It is _not_ a `nyan::Object` but rather an unit object of the game engine -which references to the `Crossbowman` `nyan::Object` to get properties from. -`malte23` is handled in the unit movement system but the speed, -healthpoints and so on are fetched for malte's unit type, which is -`Crossbowman`, managed by nyan. - - -## Modding examples - -### New resource - -Let's create a new resource. - -``` python - -# Defined in the game engine: - -Mod(): - name : text - patches : set(Patch) - -Building(): - name : text - -Resource(): - name : text - icon : file - -DropSite(): - accepted_resources : set(Resource) - - -# Above are engine features. -# Lets create content in your official game data pack now: - -Gold(Resource): - name = "Bling bling" - icon = "gold.svg" - -Food(Resource): - name = "Nom nom" - icon = "food.svg" - -TownCenter(Building, DropSite): - name = "Town Center" - accepted_resources = {Gold, Food} - - -# Now let's have a user mod that adds a new resource: - -Silicon(Resource): - name = "Silicon" - -TCSilicon(): - allowed_resources += {Silicon} - -SiliconMod(Mod): - name = "The modern age has started: Behold the microchips!" - patches = {TCSilicon} - -``` - -In the mod pack config file, `SiliconMod` is listed to be loaded. -That pack config format may be a simple .conf-style file. - -When those nyan files are loaded, the all the objects are added. Your game -engine implements that the `SiliconMod` is displayed in some mod list and -that all `patches` from activated `Mod`s are applied at game start time. - -The load order of the user supplied `Mod`s is to be determined by the -game engine. Either via some mod manager, or automatic resolution. -It's up to the engine to implement. - - -### Patching a patch example - -A user mod that patches loom to increase villager hp by 10 instead of 15. - -0. Loom is defined in the base data pack -1. The mod defines to update the original loom tech -2. The tech is researched, which applies the updated loom tech to the - villager instance of the current player - -``` python - -# Game engine defines: -Tech(): - name : text - desc : text - updates : set(Patch) - -Mod(): - name : text - patches : set(Patch) - -Ability(): - mouse_animation : file - -Unit(): - name : text - hp : int - abilities : set(Ability) - -Building(): - name : text - researches : set(Tech) - creates : set(Unit) - - -# Base game data defines: -Villager(Unit): - name = "Villager" - hp = 25 - -LoomVillagerHP(): - hp += 15 - -Loom(engine.Tech): - name = "Loom" - desc = "Research Loom to give villagers more HP" - updates = {LoomVillagerHP} - -TownCenter(Building): - researches = {Loom} - creates = {Villager} - - -# User mod decreases the HP amount: -BalanceHP(): - hp -= 5 - -LoomBalance(Mod): - name = "Balance the Loom research to give" - patches = {BalanceHP} - -# in the mod pack metadata file, LoomBalance is denoted in the index.nfo -# to be loaded into the mod list of the engine. -``` - -### Creating a new ability - -Now let's create the ability to teleport for the villager. - -Abilities are used as [entity component system](https://en.wikipedia.org/wiki/Entity_component_system). -The game engine uses sets of those to modify unit behavior. - - -* Abilities can define properties like their animation -* An ability can be added as a tech to some units at runtime - ("villagers and the tentacle monster can now teleport") -* Behavior must be implemented in the engine - * If custom behavior is required, it must be set up through a scripting API of the engine - * `nyan` can change and updated called function names etc to - activate the scripting changes, but how is up to the engine - - -``` python - -# The engine defines: -Mod(): - name : text - patches : set(Patch) - -Unit(): - name : text - hp : int - abilities : set(Ability) - -Resource(): - name : text - icon : file - -DropSite(): - accepted_resources : set(Resource) - -Animation(): - image : file - frames : int = 1 - loop : bool = true - speed : float = 15.0 - -Ability(): - animation : Animation - -CooldownAbility(Ability): - recharge_time : float - -Movement(Ability): - speed : float - instant : bool = false - range : float = inf - -CollectResource(Movement): - target : Resource - collect_animation : Animation - - -# Base game data defines: -Wood(Resource): - name = "chop chop" - icon = "wood.svg" - -VillagerWalking(Animation): - image = "walking_villager.png" - frames = 18 - -VillagerMovement(Movement): - animation = VillagerWalking - speed = 15.0 - -WoodTransport(Animation): - image = "wood_transport.png" - frames = 20 - -WoodChop(Animation): - image = "wood_transport.png" - frames = 20 - -CollectWood(CollectResource): - target = Wood - animation = WoodTransport - collect_animation = WoodChop - speed = 12.0 - -Villager(Unit): - name = "Villager" - hp = 25 - abilities += {VillagerMovement, CollectWood} - - -# Teleport mod: -TeleportBlurb(Animation): - image = "teleport_whooosh.png" - frames = 10 - speed = 2 - -Teleport(Movement, CooldownAbility): - speed = 0.0 - instant = true - recharge_time = 30.0 - range = 5 - animation = TeleportBlurb - -EnableTeleport(): - abilities += {Teleport} - -TeleportMod(Mod): - name = "Awesome teleport feature to sneak into bastions easily" - patches = {EnableTeleport} -``` - -* Why does `Teleport` inherit from both `Movement` and `CooldownAbility`? - * Teleport is another movement variant, but the cooldown timer must be mixed in. - After an ability was used, the engine checks if the `Ability` is a `CooldownAbility`, - and then deactivates the ability for some time for that unit. - When the engine checks `Teleport.extends(CooldownAbility)`, - it is true and the timer routine will run. -* Why is there an `instant` member of `Movement`? - * The game engine must support movement without pathfinding, otherwise even - movement with infinite speed would be done by pathfinding. - -This demonstrated that modding capabilities are strongly limited by the game -engine, nyan just assists you in designing a mod api in an intuitive way. diff --git a/doc/nyan_specification.md b/doc/nyan_specification.md new file mode 100644 index 0000000..ab0f1ab --- /dev/null +++ b/doc/nyan_specification.md @@ -0,0 +1,606 @@ +# nyan Specification + +## Table of Contents + +* [1. Quick Reference](#quick-reference) +* [2. Object](#object) +* [3. Member](#member) +* [4. Patch](#patch) +* [5. Namespace](#namespace) + * [5.1 Importing](#importing) +* [6. Examples](#examples) + * [2.1 Patching a Patch](#patching-a-patch) + * [2.2 Multi Inheritance](#multi-inheritance) + +## Quick Reference + +``` python +# 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[+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 + ... +``` + +## Object + +An **object** declares a named storage space for key-value pairs. + +```python +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](#inheritance)** +* An arbritrary number of **[member](#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** iff 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. + +```python +ObjName(ParentObj, OtherParentObj, ...): + pass +``` + +Objects can be referenced by a fully qualifies 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](#namespace)). + +```python +# 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(): + ... +``` + + +## Member + +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. + +```python +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](member_types.md). + +```python +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. + +```python +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. + +```python +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](member_types.md). + +```python +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](#patch)**. + + +## 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 similar to a normal object with the addition of +a **target**. The patch target must be written in angled brackets +after the object name. The defined target cannot be changed. + +```python +SomeObject(): + ... + +PatchName(): + ... +``` + +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](member_types.md). + +A patch can be applied multiple times. On every application all +operations are executed. + +```python +SomeObject(): + member_name : int = 7 + other_member : int = 23 + +SomePatch(): + # 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. + +```python +SomeObject(): + member_name : int = 7 + other_member : int = 23 + +# Targets SomeObject +SomePatch(): + member_name = 50 + other_member += 19 + +# Targets SomePatch +OtherPatch(): + # 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. + +```python +SomeObject(): + member_name : int = 7 + +SomePatch(): + member_name -= 3 + +OtherPatch(): + # 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. + +```python +SomeObject(): + member_name : int = 7 + +SomePatch(): + member_name -= 3 + +OtherPatch(): + # 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. + +```python +SomeObject(): + member_name : int = 7 + +SomePatch(): + member_name -= 3 + +OtherPatch(): + member_name = 1 + +FixOtherPatch(): + # 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 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. + +```python +SomeObject(): + ... + +PatchName[+AdditionalParent, AnotherParent+, ...](): + ... +``` + +Adding a parent must not induce name clashes of members (see the +[multiple inheritance example](#multi-inheritance)). + +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. + +```python +SomeObject(): + member_name : int = 7 + +# Patch by definition +SomePatch(): + 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. + +## Namespace + +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 +``` + + +### Importing + +To reference nyan objects in other files, a namespace they are +in has to be imported using the `import` keyword. + +``` python +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. + + +``` python +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. + +``` python +import thuglife.units.backstreet as thugs + +Frank(thugs.DrugDealer): + speciality = "Meth" +``` + +## Examples + +### Patching a Patch + +A user mod that patches loom to increase villager hp by 10 instead of 15. + +0. Loom is defined in the base data pack +1. The mod defines to update the original loom tech +2. The tech is researched, which applies the updated loom tech to the + villager instance of the current player + +``` python +# Base game data defines: +Villager(Unit): + name = "Villager" + hp = 25 + +LoomVillagerHP(): + hp += 15 + +# User mod decreases the HP amount: +BalanceHP(): + hp -= 5 +``` + +This demonstrated that modding capabilities are strongly limited by the game +engine, nyan just assists you in designing a mod api in an intuitive way. + + +### Multi inheritance + +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`: + +``` python +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. + +``` python +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 by `C3` is `[A, B, Top, C]` +* When in `LOLWhat` the `C.entry` value is requested, that list is walked + through until a value declaration for each member was found: + * `A` declares `otherentry` and `specialentry`, it changes `entry` + * `B` declares `otherentry` and changes `entry` + * 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 + * `Top` declares `entry` + * `C` declares `entry` and `otherentry` + * Here, nyan detects that `entry` and `otherentry` are declared again + * The access to `entry` must hence be qualified, too +* nyan concludes that all accesses must be qualified, + except to `specialentry`, as only one declaration was found +* The qualification is done by prefixing the precedes 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 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 gone through until the declaration of the requested member + was found + * `LOLWhat` did not declare it + * `A` did 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 the `entry` either + * `Top` does declare it, now the recursion goes back the other way + * `Top` defined the value of `entry` to `10` + * `B` wants to subtract `3`, so `entry` is `7` + * `A` adds `5`, so `entry` is `12` + * `LOLWhat` adds `1`, `entry` is `13` + * `OHNoes` adds `1` as well, and `entry` is returned to be `14` diff --git a/extra/syntax_highlighting/kate/nyan.xml b/extra/syntax_highlighting/kate/nyan.xml index 0aa3419..e0ea12e 100644 --- a/extra/syntax_highlighting/kate/nyan.xml +++ b/extra/syntax_highlighting/kate/nyan.xml @@ -15,7 +15,7 @@ set orderedset - + dict True diff --git a/version b/version new file mode 100644 index 0000000..0ea3a94 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.2.0