diff --git a/docs/zengin/scripts/general_info/.pages b/docs/zengin/scripts/general_info/.pages index 8bad19bcc6..3935c9ffcb 100644 --- a/docs/zengin/scripts/general_info/.pages +++ b/docs/zengin/scripts/general_info/.pages @@ -1,3 +1,4 @@ nav: - daedalus.md - - ... \ No newline at end of file + - pitfalls.md + - ... diff --git a/docs/zengin/scripts/general_info/compilation.md b/docs/zengin/scripts/general_info/compilation.md index b10c41a4d7..52eb5aa8c6 100644 --- a/docs/zengin/scripts/general_info/compilation.md +++ b/docs/zengin/scripts/general_info/compilation.md @@ -36,7 +36,7 @@ The `.src` files are simple text files that contain the paths to the `.d` files **Example file structure:** -``` title="<gothic-root>/_work/Data/" +``` title="/_work/Data/" └── Scripts    ├── _compiled    └── content diff --git a/docs/zengin/scripts/general_info/daedalus.md b/docs/zengin/scripts/general_info/daedalus.md index b56daaa07d..2e26c3bf82 100644 --- a/docs/zengin/scripts/general_info/daedalus.md +++ b/docs/zengin/scripts/general_info/daedalus.md @@ -1,110 +1,121 @@ -# Daedalus Script Language +# Daedalus Scripting Language -ZenGin engine uses its own script language called Daedalus[^1]. The language is used to define the game logic, dialogues, AI, missions, and other game-related content. +ZenGin engine uses its own script language called Daedalus[^1]. The language is used to define some of the game logic, dialogues, AI, quests, NPC AI, sounds, menus and other game-related content. -Daedalus syntax is a mix of `C` and `C++`. The script language is not case-sensitive and whitespace is ignored. +Daedalus has a `C`-like syntax and is case insensitive. ## Identifiers and Keywords -Identifiers are names for variables, constants, instances, prototypes, classes and functions. An identifier is a sequence of letters and digits. The first character must be a letter. Thereafter, are allowed: letters, digits `0` to `9` and underscores. The length of identifiers is not restricted. +Identifiers are names for variables, constants, instances, prototypes, classes and functions. -Keywords are: +Keywords are: - var const if else class prototype instance return null nofunc int float string void func +- var +- const +- if +- else +- class +- prototype +- instance +- func +- return +- int +- float +- string +- void +- null +- nofunc -Keywords are reserved and can not be used as identifiers. +> ⚠️ See [Pitfalls](pitfalls.md#identifiers-and-keywords) +## Comments -## Commentaries +Daedalus supports single line and multiline comments. -The string `/*` begins a comment, which is closed by the string `*/`. +### Multiline comments ```dae -/* This is a comment - which can be spread over several lines */ +/* This is a multi + line comment +*/ ``` -The string `//` begins a comment, which goes until the end of the line. +### Single line comments ```dae -// This is a comment +// This is a single line comment ``` -The strings `//` and `/*` within a comment as well as `*/` after a line comment have no further meaning and are handled like other signs. +!!! Note + There is a special usecase for single line comments. A single line comment on the same line as the `AI_Output` function will be parsed as the output. + ```dae + AI_Output(self, hero, "Info_Diego_Gamestart_11_00"); //I'm Diego. + AI_Output(hero, self, "Info_Diego_Gamestart_15_01"); //I'm... + ``` ## Variables and Constants +> ⚠️ See [Pitfalls](pitfalls.md#variables) + ### Data Types -| data type | default value | description | -|-----------|--------------------------------------------------------------------|-------------------------------------------------------------------------------| -| int | 0 | integer number type - standard 32-bit signed integer | -| float | 0.0 | floating point number type - standard IEEE-754 32-bit floating point number | -| string | "" | string type | -| void | no value type | used to denote a function that returns nothing | -| func | 0 :octicons-question-16:{ title="I think it should be -1 NOFUNC" instead } | function type used for setting callbacks and passing functions into functions | +| Data type | Default value | Description | +|-----------|---------------|--------------------------------------------------------------------------------| +| int | `0` | Integer — standard 32-bit signed integer | +| float | `0.0` | Floating-point — IEEE-754 32-bit (single precision) | +| string | `""` | String | +| void | *n/a* | No value type; used to denote a function that returns nothing | +| func | `-1` (NOFUNC) | Function type — used for setting callbacks and passing functions as arguments | !!! Note - Notice that the classes can also be used as variable and constant types. - -### Variables -In daedalus, variable values are saved in save game files. + User-defined classes can also be used as variable and constant types. -The declaration of a variable is initiated by the keyword `var`. -```dae -var int x1; -var int x2, x3, x4; -``` +### Variables +Variables are named identifiers that hold a value of a certain type. -!!! Danger - The assignment is not allowed alongside with declarations. The assignment must be done in a separate statement. +**Persistence.** Variables persist across save/load cycles. +Variables are declared using the `var` keyword: ```dae -// Correct: var int x1; -x1 = 42; - -// Wrong: -var int x2 = 42; +var int x2, x3, x4; ``` #### Variable Arrays -Arrays are declared by adding square brackets with the size after the name. +Arrays are declared by appending square brackets with the size after the name: ```dae var int x[10]; x[0] = 42; ``` -The first element of an array has the index 0. - -!!! Danger - It is not possible to access array `x[i]` with a variable index i, but only with a constant value. - +Array indexing starts at `0`. ### Constants -Constants have predefined values, that can be changed, but won't be saved in save game files and will be reset to the default value after loading a save game. +Constants must be initialized with a value. They can be reassigned at runtime, but any changes are lost when a save game is loaded — their value resets to the original declaration. + +**Persistence.** Constants retain their declared value across save/load cycles; runtime changes do not persist. -The declaration of a constant by the keyword `const`. +Constants are declared using the `const` keyword: ```dae const int max_level = 100; -max_level = 200; // allowed but not recommended (value will be lost) +max_level = 200; // allowed but not recommended (value resets on load) ``` #### Constant Arrays -Constants can also be arrays, but all elements must be defined at the time of declaration. +Arrays can also be defined as constants, in which case all elements must be specified at the time of declaration: ```dae const int values[3] = { - 1, - 2, + 1, + 2, 3 }; ``` -### Global, Local -Variables and constants can be declared globally or locally. +## Scoping +Daedalus does not really support scoping. But it is useful to think about scopes as concepts in Daedalus. -If a variable is declared outside a function, it is global, and can be accessed from anywhere in the script. If a variable is declared inside a function, it is local, and can only be accessed from within the function. +If a variable is declared outside a function, it is in the global, and can be accessed from anywhere in the scripts. If a variable is declared inside a function, it is in the local scope, and can only be accessed from within the function. ```dae var int global; func void test_1() -{ +{ var int local; global = 42; }; @@ -113,56 +124,85 @@ func void test_2() { local = 42; // Error: `local` is not defined }; -``` +``` + +!!! Note + Traditional C style scoping is not what is happening here. The `local` variable inside the `test_1` function is really a global symbol `test_1.local`, which can be used with extensions, that allow you to manipulate symbols sing their name as a string literal. ## Operators -An operator "calculates" the resulting value from one or two values. The following operators are available in Daedalus: -``` - Calculation: - + Addition - - Subtraction - * Multiplication - / Division - % Euclidean division (Modulo) - - Comparison: - < smaller than - <= smaller or equal to - > greater than - >= greater or equal to - == Equality - != Inequality - - Boolean: - ! not - && and - || or - - - Bitwise: - & and - | or - - Sign: - + positive - - negative -``` - -!!! Danger - The operators work only with integer values. +Daedalus supports a wide range of unary and binary operators. The operators are listed below grouped by category. + +### Unary (Prefix) Operators +| Operator | Description | +|----------|------------------------------------------------------| +| `+` | Positive (identity — returns the value unchanged) | +| `-` | Negation (negates the sign of the operand) | +| `!` | Logical NOT (inverts a boolean condition) | +| `~` | Bitwise NOT | + +### Binary (Infix) Operators — Arithmetic +| Operator | Description | +|----------|---------------------------| +| `+` | Addition | +| `-` | Subtraction | +| `*` | Multiplication | +| `/` | Division | +| `%` | Modulo (remainder) | + +### Binary (Infix) Operators — Comparison +| Operator | Description | +|----------|--------------------------------| +| `<` | Less than | +| `<=` | Less than or equal to | +| `>` | Greater than | +| `>=` | Greater than or equal to | +| `==` | Equality | +| `!=` | Inequality | + +### Binary (Infix) Operators — Logical +| Operator | Description | +|----------|--------------------------------------| +| `||` | Logical OR (short-circuit evaluation) | +| `&&` | Logical AND (short-circuit evaluation)| + +### Binary (Infix) Operators — Bitwise +| Operator | Description | +|----------|-----------------------| +| `|` | Bitwise OR | +| `&` | Bitwise AND | +| `<<` | Bitwise left shift | +| `>>` | Bitwise right shift | + +### Compound Assignment Operators +| Operator | Equivalent To | Description | +|----------|------------------------|---------------------------------------| +| `=` | — | Simple assignment | +| `+=` | `x = x + y` | Add and assign | +| `-=` | `x = x - y` | Subtract and assign | +| `*=` | `x = x * y` | Multiply and assign | +| `/=` | `x = x / y` | Divide and assign | +| `<<=` | `x = x << y` | Left shift and assign | +| `>>=` | `x = x >> y` | Right shift and assign | +| `&=` | `x = x & y` | Bitwise AND and assign | +| `|=` | `x = x \| y` | Bitwise OR and assign | + +### Precedence +Operator precedence is the same as it is in `C`. + +> ⚠️ See [Pitfalls](pitfalls.md#operators) ## Control Flow ### if statement The if-statement is similar to C/C++. ```dae -func void example_if_statement(var int a) +func void example_if_statement(var int a) { - if (a < 5) + if (a < 5) { // .. } - else if (a == 5) + else if (a == 5) { // .. } @@ -175,7 +215,7 @@ func void example_if_statement(var int a) ### return statement -In functions, which return a value, a return assignment is used. +A `return` statement ends a function and sends a value back to the code that called it. ```dae func int example_return_statement() @@ -183,8 +223,6 @@ func int example_return_statement() return 42; }; ``` - - ## Functions ### Definition @@ -204,16 +242,13 @@ func type name ( var type param1, ... ) { // some code }; - ``` + ``` ### Parameters -The length of the parameter list is unlimited, but should be kept as short as possible for memory capacity reasons. - -!!! Warning - Arrays are not allowed as parameter transfers. +The length of the parameter list is unlimited, but should be kept as short as possible for memory capacity reasons. ### Function calls -Functions are called as usual in C++. Also with their identifier as well as a mandatory parameter bracket. +Functions are called by writing the function identifier followed by the argument list in parentheses `()`. ```dae func void bar() @@ -222,8 +257,13 @@ func void bar() }; ``` +### Return +ZenGin compiler does not do any control flow analysis, this means it cannot detect mistakes, where the data stack would become corrupted. + +> ⚠️ See [Pitfalls](pitfalls.md#functions) + ## Classes -Classes usually mirror classes/structs on the engine side. They are defined by the keyword `class` and can contain variables; +Classes usually mirror classes/structs on the engine side. They are defined by the keyword `class` and can contain member variables. ```dae class Foo { var int i1; @@ -233,7 +273,7 @@ class Foo { ``` ### Prototypes -Prototypes are templates for classes. They can be used to create instances with predefined values. The prototype definition is initiated by the keyword `prototype`. +Prototypes are reusable templates of instances. They can be used to create instances with predefined values. The prototype definition is initiated by the keyword `prototype`. The type of the prototype (or its parent) is defined in the parenheses `()`. ```dae prototype FooProtoType (Foo) @@ -244,7 +284,10 @@ prototype FooProtoType (Foo) ``` ### Instances -Instances can be created from classes or prototypes. +Instances represent engine instances of classes. An instance's parent can be a class or a prototype. This construct is used to define various objects (items, NPCs, dialogues). + +When an instance is created from a **class**, all members must be explicitly assigned a value. Any member that is not set in the instance body is initialized with the class's default value (e.g. `0` for `int`, `""` for `string`, etc.). + ```dae instance FooInstance (Foo) { @@ -253,16 +296,13 @@ instance FooInstance (Foo) }; ``` +When an instance is created from a **prototype**, the instance inherits all values from the prototype and can override any of them. Members that are not explicitly set in the instance body are inherited from the prototype as-is. ```dae instance FooInstance (FooProtoType) { - i1 = 100; + i1 = 100; // overridden from prototype (was 42) + // s1 is inherited from prototype ("Hello World") }; -``` - -!!! Note - The instance definition can be used to overwrite the default values of the prototype. - - -[^1]: The inspiration was taken form text written by Piranha Bytes. Its translation can be found on [Gothic MDK website](https://mdk.gothicarchive.org/docs/skripte/gothic_skriptsprache.htm). \ No newline at end of file +``` +[^1]: The inspiration was taken form text written by Piranha Bytes. Its translation can be found on [Gothic MDK website](https://mdk.gothicarchive.org/docs/skripte/gothic_skriptsprache.htm). diff --git a/docs/zengin/scripts/general_info/pitfalls.md b/docs/zengin/scripts/general_info/pitfalls.md new file mode 100644 index 0000000000..c8e3513855 --- /dev/null +++ b/docs/zengin/scripts/general_info/pitfalls.md @@ -0,0 +1,201 @@ +# Quirks & Pitfalls + +A collection of warnings, quirks, and gotchas when working with the Daedalus scripting language. + +## Identifiers and Keywords + +### Non-standard identifier names +The Daedalus compiler accepts identifiers that do not start with a letter or contain non-ASCII characters - for example `const string 12AS = "Daedalus...";` or `const string 1 = "0"`. Even though this is possible, it should **not** be used. Such identifiers lead to unreadable code and may break tooling (editors, linters, generators). + +### Keywords as identifiers +Even though keywords can be used as identifiers outside of their expected context (e.g., `var int instance;` where `instance` is the variable name), this should **not** be done. Keywords only have special meaning at specific syntactic positions, so the compiler accepts them elsewhere - but doing so makes code harder to read and may cause issues with tooling. + +## Variables + +### Variable declarations don't initialize values at runtime +Variable declaration itself does not initialize the value, it only tells the compiler that it exists in that scope (global or local). This can be a source of confusion: + +```dae +func int count() { + var int i; + + i += get_some_val(); + i += get_other_val(); + + return i; +}; +``` + +When this function is first invoked, the `count.i` variable will be zero (initialized by the compiler even before execution). Then `some_val` would be added, then `other_val` would be added and the result on the first call would be what you expect. But when this function is called for the second time, the variable `count.i` already has the previous value in it. The declaration statement `var int i` has **no runtime execution**. So the result would be double the previous one. + +The correct way to write this in Daedalus would be: + +```dae +func int count() { + var int i; i = 0; + + i += get_some_val(); + i += get_other_val(); + + return i; +}; +``` + +### Avoid mixed type comma separated declarations +```dae +var int x1, var float f3, var string s3; +``` +This variant is technically possible but should be avoided. It exists only as an artifact of code reuse in the ZenGin parser. + +### Declaration with an initializer +Since the variable declaration only tells the compiler such symbol exists, it cannot be initialized right away. You must do an extra assignment statement. +```dae +// Correct: +var int x1; +x1 = 42; + +// Wrong: +var int x2 = 42; +``` + +## Arrays + +### Variable indices are not supported +Array elements can only be accessed with a constant index - variable indices (e.g. `x[i]`) are not supported. + +### Maximum array indexable via `[]` is 255, though max size is 4096 +Although the maximum array size in Daedalus is 4096 elements, the maximum indexable value via the `[]` operator is **255**. + +### Constant arrays cannot be indexed with `[]` +In vanilla Daedalus, the `[]` operator cannot be used to access elements of a constant array at all - not even with a constant index. You must declare the elemnts individually as variables if you need runtime element access. + +## Integer values +Daedalus `int` is a signed 32-bit integer (`-2 147 483 648` to `2 147 483 647`). Writing a number outside this range produces **different results depending on context**: +```dae +var int x = 9999999999999; // overflows to an unexpected value +var int z = 2147483648; // wraps around to INT_MIN (-2147483648) +var int w = 2147483647; // OK — max int value +``` +ZenGin does **not** warn about this, so large constants can silently produce wrong results at runtime. + +## Operators + +### Floating-point expressions only work at compile time +The arithmetic and comparison operators work only with **integer** values. Floating-point expression work only in "compile time" expressions, so only in `const float` expressions. + +## Functions + +### Arrays cannot be passed into a function +Arrays cannot be passed as function parameters. + +## Data stack issues +There are few classes of data stack issues, pitfalls and quirks in default Daedalus. + +### Broken exernals +[External functions]() are engine-implemented routines callable from Daedalus code. Several ZenGin externals suffer from data stack corruption due to two primary classes of bugs: + +- *Mismatched Registration:* The external's metadata incorrectly declares the number of arguments it consumes or return values it pushes. This leaves residual operands on the stack or silently consumes unowned values, corrupting subsequent operations. +- *Incomplete Control Flow:* Certain implementations fail to push a return value along specific execution paths. When Daedalus evaluates conditions or branches using these externals, the runtime stack desynchronizes from the compiler's expectations. + +### Missing return values corrupt the data stack +ZenGin compiler does not do any control flow analysis, this means it cannot detect mistakes where the data stack would become corrupted. + +```dae +func int some_condition() { + if something_happened() { + return TRUE; + }; +}; +``` + +In this case, one branch of execution returns `TRUE`, but if `something_happened()` returns false, nothing is returned from this function. This results in a corrupted data stack. + +ZenGin has a stack underflow protection (a hack, really): if a value is popped off the stack even though the stack is empty, instead of underflowing, a 0 is returned. Relying on this functionality is not good practice, since you can easily rely on the automatic zero being there while the stack can still have some values on it - and you'll be popping values that should stay there. This creates **undefined behaviour** and should be avoided at all costs. + +A more reasonable implementation would be: + +```dae +func int some_condition() { + if something_happened() { + return TRUE; + } else { + return FALSE; + }; +}; +``` + +Or even better: + +```dae +func int some_condition() { + return something_happened(); +}; +``` + +!!! Note + This is not only better, because all branches of execution return a value (there are no branches here), but it is also less code. When Daedalus is compiled, it is directly converted into bytecode - no optimisation happens. This means, shorter code, usually means it will run faster in game. + +Due to this lack of control flow analysis and any rigorous checks at compile time, your scripts can compile even when there are quite serious problems. If you, for example, leave a value just as a statement, that pushes that value onto the data stack. +```dae +const int my_val = 42; +func void do_something() { + // ... + my_val; // valid! pushes 42 onto the stack — forgotten value. + TRUE == FALSE; // also valid — evaluates to false, pushes false to the stack, never consumed. + // ... +}; +``` +Whenever `do_something()` is called it leaves an extra value `my_val` on the data stack. Since there is no code that consumes this value (there could be, more on that later - but it is not a good way of doing things) it stays on the stack forever (until the engine clears the entire stack). +This is, of course, a problem and can lead to stack overflows. + +### Function calls as statements +Daedalus does not support function calls as statements, when you call a function in daedalus and it returns a value, even if you do not assign it to anything. This results in values being pushed to the stack, that will not be consumed and can result in a stack overflow. Or it will be consumed by a function with an error in it... + +In the following example, each add_item returns an int. +```dae +// let's call `func int add_item(var C_NPC npc, var C_ITEM itm, var int amnt)` +func int create_trader_items(var C_NPC trader) { + add_item(trader, ItFo_Beer, 6); + add_item(trader, ItFo_Apple, 6); + add_item(trader, ItFo_Mutton, 6); + + // ... many more calls + + add_item(trader, ItFo_Ham, 6); +}; +``` +At the end of the function the stack is going to be full of extra values, that do not get popped. Depending on the codepath, this could easily result in a stack overflow. + +### Automatic stack underflow protection abuse +You may notice that vanilla scripts frequently abuse this data stack hack: + +```dae +FUNC INT Info_Diego_Gamestart_Condition() +{ + if (Kapitel < 2) + { + return TRUE; + }; +}; +``` + +This pattern works most of the time because dialogue conditions are evaluated by the engine at initialization time — and the engine clears the data stack before each call. So when the condition `(Kapitel < 2)` evaluates to false, the stack remains empty, and the engine's underflow protection silently returns `0`. + +However, the stack is **not** cleared when such a function is called from within Daedalus code (i.e., not invoked directly by the engine). Consider this example: + +```dae +func void example_func() { + // some code + some_function(); // func int some_function() + + if (cond_1 || Info_Diego_Gamestart_Condition()) && Npc_KnowsInfo(self) { + // something + } else { + // something else + }; +}; +``` + +In this case, `some_function()` leaves its return value on the data stack. When the `if` condition is evaluated and `Info_Diego_Gamestart_Condition()` happens to return false, the `||` operator pops two values from the stack. But instead of popping `cond_1` and the result of the condition check, it pops `cond_1` and the **leftover return value** of `some_function()` — which is the wrong operand entirely. + +This leads to undefined and highly unpredictable behaviour. Avoid relying on automatic underflow protection at all costs.