AGS Scripting language architecture

Started by Sakspapir, Sun 23/05/2021 08:24:56

Previous topic - Next topic

Sakspapir

Hi everyone!

First of all, sorry for the vague terminology in this thread. It does not stem from laziness I assure you, but rather from ignorance. With this thread I am hoping to fix that ignorance.

My question is related to the general architecture (or philosophy, if you will) of the AGS scripting language. I have read the Scripting tutorial and also looked through the Forums and Wiki, but I can't seem to find information on how the scripting language in general. I guess what I'm looking for is a tutorial or manual that explains the concepts of AGS scripting language (e.g. .ash/.asc files, script modules, etc) and how they relate to each other. Typical questions I would like to have answered are:

* How does the scripting modules relate to the GlobalScript?
* What is the relationship between header- and script files? Coming from c/c++ it looks wierd to import functions in the header.
* In what order is the code compiled and built?

So, is there such information available anywhere? Like a big AGS scripting language programming bible of sorts?

Crimson Wizard

#1
The existing manual is definitely lacking in this, but we are gradually improving it and the latest version may be found here:
https://github.com/adventuregamestudio/ags-manual/wiki
Specifically script language is discussed here:
https://github.com/adventuregamestudio/ags-manual/wiki#scripting

But it was traditionally focused on non-programming savvy user perspective so some things may be explained not scientifically, so to speak.

On your questions:

Quote from: Sakspapir on Sun 23/05/2021 08:24:56
* How does the scripting modules relate to the GlobalScript?

All the scripting modules work alike. GlobalScript is a historical thing, and it's main purpose today is to contain event handlers for global objects (characters, gui, inventory items) because engine is programmed to only look for these handlers in there. That restriction does not make much sense now, and may probably be removed at some point.

Quote from: Sakspapir on Sun 23/05/2021 08:24:56
* What is the relationship between header- and script files? Coming from c/c++ it looks wierd to import functions in the header.

Before the script gets compiled it's own header and all the headers above it are virtually prepended to it. From a C/C++ perspective this works as if you had "#include" for each header that is paired with the script and located above in the list of scripts.
This means that if you have scripts:
A
B
C
D
Then script D will "include" everything in headers A,B,C,D, script B will "include" only headers A,B and so on.
Scripts do not see contents of each other bodies, only other headers.

"import" command is practically an equivalent to "extern" in C: it tells that a variable exists but defined (allocated) somewhere else. This prevents variables from duplicating (normally you don't place normal variable declaration in the header).
Same for functions - it just means that function is somewhere else and compiler can expect it will be available at the time of linking.
There's a related article: https://github.com/adventuregamestudio/ags-manual/wiki/ImportingFunctionsAndVariables

Quote from: Sakspapir on Sun 23/05/2021 08:24:56
* In what order is the code compiled and built?

I am not sure whether you are asking about the code within one module, or order of compiling the modules?

Currently compiler only makes one pass through the code, so it's as restrictive as in C to the order of declarations.

As for order of modules, I think they are compiled in the order in their list, but that's not really important.

AGS is special in a way that "linking" is performed not by the editor, but by the script interpreter (in the engine) at runtime. So linking errors (missing variable/function definitions) will only be found as you run the game.

Snarky

#2
Just a small quibble: I don't think these are really questions about the AGS scripting language, but rather about how scripts are combined during building (I think this corresponds to the "linking" step in the build process, but I've never had a firm grasp of compilers and all that).

Anyway, I'm not aware that this is covered by any comprehensive explanation anywhere.

To address your questions (reordered for clarity):

In what order is the code compiled and built?
AGS compilation order is (at the moment) strictly one-pass and linear, starting from the top script and working its way down to the bottom (GlobalScript), and then finally the room scripts (presumably in order, but that doesn't make a difference since they cannot refer to each other). Therefore, if you want to use anything from one script in another, the first script has to be above it in the script list. (Even if you use folders to organize your script modules, it's still simply the order they're listed.)

Also, dialogs get converted to AGS script and placed somewhere in the list (Edit: After GlobalScript, as CW corrects).

So if you have your scripts like:

  • Library modules (script folder):

    • Tween
    • Tumbleweed
  • Custom modules (script folder):

    • ChapterLogic
    • SpellInventory
  • GlobalScript
  • Rooms:

    • Room 1
    • Room 2
Then they will be built as:

  • Tween
  • Tumbleweed
  • ChapterLogic
  • SpellInventory
  • GlobalScript
  • Dialog scripts
  • Room 1
  • Room 2
While CW says the order of compilation isn't important, it is important that you cannot reference things lower in the list than where you are (or, indeed, anything below where you are inside the script), because it means that, for example, you cannot use anything in the SpellInventory script in the ChapterLogic script. Sometimes this restriction can cause problems, and you'll need to restructure your scripts.

Note that the build order is not necessarily the same as the order in which scripts are run. That's determined by function calls and events. Often, the first thing that happens in a game loop (at least as far as the script is concerned) is that some function in GlobalScript is called by the engine, and then that function calls some of the functions in the other script modules. (However, if you have multiple copies of the standard game logic functions, like repeatedly_execute() or on_key_press(), they will be run in script order, starting with the topmost script.)

How do script modules relate to the GlobalScript?
Script modules are simply separate files you can put code in. In principle, you could have all the code in GlobalScript, but as a game becomes bigger, you typically want to keep things better organized. Another benefit of script modules is that people can write and share modules for common tasks, and you can use them like a "library" without needing to understand how they work (like Tween and Tumbleweed).

There are two things you need to be aware of when it comes to script modules vs. GlobalScript:

1. All the event handlers (things like like btnOk.Click() and oCoin.Look()) must go in GlobalScript; that's the only place the compiler will look for them.

2. Any code you put in a script module is, by default, not visible in any other script. That means you can have the same "global" variable names (variables declared outside of any functions) in different scripts, for example; they'll stay separate. It also means that if you do want a function or a variable to be visible, you need to make sure to share it, which you do using the export and import keywords.

This sharing is a two-step process. First, variables need to be exported to indicate that they should be shared with other scripts. (You don't need to export functions; conceptually, you can say that they are exported by default.) Then, both functions and variables need to be imported to the script where you want to use them: essentially this links the name to the function/variable inside the other script. To make importing easier (so that you don't need to put a long list of import statements at the top of every script that wants to use them), we use headers.

What is the relationship between header- and script files?
Well, there are two ways to think about it.

The "conceptual" way is that the header describes the interface (API) of the script: how it can be used in other scripts. So this is where we put any type declarations (for any new macros, enums, or structs we've added for general use), and the import statements for functions and variables we want to share. Anything we put in the header can be used by other scripts (lower in the list), while anything that's just in the .asc file cannot (or at least, should not) be used by other scripts.

The other way is how it actually works "technically": basically, the header is copied at the top of every script below it in the list. So in the example from above, what you actually end up with after the compilation is (Edit: corrected):

  • (Tween.ash) + Tween.asc
  • Tween.ash + (Tumbleweed.ash) + Tumbleweed.asc
  • Tween.ash + Tumbleweed.ash + (ChapterLogic.ash) + Chapterlogic.asc
  • Tween.ash + Tumbleweed.ash + Chapterlogic.ash + (SpellInventory.ash) + SpellInventory.asc
  • Tween.ash + Tumbleweed.ash + Chapterlogic.ash + SpellInventory.ash + (Globalscript.ash) + GlobalScript.asc
  • Tween.ash + Tumbleweed.ash + Chapterlogic.ash + SpellInventory.ash + GlobalScript.ash + DialogScripts.asc (converted by AGS)
  • Tween.ash + Tumbleweed.ash + Chapterlogic.ash + SpellInventory.ash + GlobalScript.ash + Room1.asc
  • Tween.ash + Tumbleweed.ash + Chapterlogic.ash + SpellInventory.ash + GlobalScript.ash + Room2.asc
That's why there are import statements in the header: the headers actually end up in the other scripts, and tells them to import stuff from the scripts above. (The type declarations in the header are also used in the script itself, but I take it the import statements are ignored or have no effect, so I've put those in parentheses.)

Therefore, it's also important not to put variable declarations in the headers. If you put something like "int myNumber;" in Tween.ash, you'll end up with seven copies of it, one for each script below it in the list. And these will be different variables with the same name, they won't refer to the same myNumber.

(As already mentioned, you should put type declarations, defining things like enum and struct in the headerâ€"if you want them to be available in other scriptsâ€"because they simply describe a format, they don't create a value.)

Crimson Wizard

Snarky, there's no "DialogScripts.ash" header, and DialogScripts.asc also includes global script header.

Dialog scripts and room scripts are on same level and include all the headers (but not each other).

Sakspapir

Thanks a lot! This is exactly the kind of information I was looking for.

Somehow you guys seemed to understand my clumsily formulated questions and answered them in a very informative way, so thanks.

I realize that AGS is intended to be accessible for everyone, and not just programming gurus (which I think AGS does very well). However, these concepts of scripts being “over” each other is a concept that I consider vital to developing a structurally sound code base, and probably deserves some focus in the scripting tutorials.

Anyways, thanks a million for the answers, it was truly helpful!

Snarky

Quote from: Crimson Wizard on Sun 23/05/2021 09:50:56
Snarky, there's no "DialogScripts.ash" header, and DialogScripts.asc also includes global script header.

Dialog scripts and room scripts are on same level and include all the headers (but not each other).

Right, I said I wasn't quite sure about that part. The reason I thought they were above GlobalScript (and had some kind of header) was that somehow GlobalScript knows about the dialogs that exist, so that you can call for example dMerchant.Start(); (Though I suspect you could call the same line from any script module as well.)

So does AGS extract the list of dialogs somehow and make it accessible to all scripts? Would that not be a kind of "dialog script header"?

Crimson Wizard

#6
To clarify, there are 4 hidden scripts that are generated internally:

1. agsdefns.sh - a header that contains engine API declaration;
2. Autogenerated.ash - a header that contains all the global game declarations: characters, dialogs, gui and so on.
Real output example under spoiler:
Spoiler

Code: ags

import AudioClip aGarbage;
import AudioClip aJhongray_nupogodi;
enum AudioType {
  eAudioTypeAmbientSound = 1,
  eAudioTypeMusic = 2,
  eAudioTypeSound = 3,
  eAudioTypezzz = 4
};
import Character character[3];
import Character cEgo;
import Character cChar1;
import Character cTestANSIName;
enum CursorMode {
  eModeWalkto = 0,
  eModeLookat = 1,
  eModeInteract = 2,
  eModeTalkto = 3,
  eModeUseinv = 4,
  eModePickup = 5,
  eModePointer = 6,
  eModeWait = 7,
  eModeUsermode1 = 8,
  eModeUsermode2 = 9
};
import Dialog dialog[4];
import Dialog dDialog1;
import Dialog dDialog2;
import Dialog dDialog3;
import Dialog dDialog4;
enum FontType {
  eFontNormal = 0,
  eFontSpeech = 1,
  eFontSpeechOutline = 2
};
import GUI gui[10];
import GUI gStatusline;
#define STATUSLINE FindGUIID("STATUSLINE")
import Label lblGameName;
import Label lblScore;
import Label lblOverHotspot;
import GUI gIconbar;
#define ICONBAR FindGUIID("ICONBAR")
import Button btnIconWalk;
import Button btnIconExamine;
import Button btnIconInteract;
import Button btnIconTalk;
import Button btnIconInv;
import Button btnIconCurInv;
import Button btnIconSave;
import Button btnIconLoad;
import Button btnIconExit;
import Button btnIconAbout;
import GUI gInventory;
#define INVENTORY FindGUIID("INVENTORY")
import InvWindow invCustom;
import Button btnInvSelect;
import Button btnInvLook;
import Button btnInvOK;
import Button btnInvDown;
import Button btnInvUp;
import GUI gPanel;
#define PANEL FindGUIID("PANEL")
import Button btnResume;
import Button btnSave;
import Button btnAbout;
import Button btnQuit;
import Slider sldAudio;
import Slider sldVoice;
import Slider sldGamma;
import Button btnDefault;
import Button btnLoad;
import Label lblVolume;
import Label lblVoice;
import Label lblGamma;
import Button btnVoice;
import Slider sldSpeed;
import Label lblSpeed;
import Button btnRestart;
import GUI gRestart;
#define RESTART FindGUIID("RESTART")
import Label lRestartText;
import Button btnRestartYes;
import Button btnRestartNo;
import GUI gSaveGame;
#define SAVEGAME FindGUIID("SAVEGAME")
import Label lblSaveText;
import TextBox txtNewSaveName;
import Button btnSaveGame;
import Button btnCancelSave;
import ListBox lstSaveGamesList;
import Button btnDeleteSave;
import GUI gRestoreGame;
#define RESTOREGAME FindGUIID("RESTOREGAME")
import Label lblRestoreText;
import Button btnRestoreGame;
import Button btnCancelRestore;
import ListBox lstRestoreGamesList;
import GUI gTextBorder;
#define TEXTBORDER FindGUIID("TEXTBORDER")
import GUI gExitGame;
#define EXITGAME FindGUIID("EXITGAME")
import Button btnConfirmedQuit;
import Button btnPlay;
import Label lQuitWindowText;
import GUI gGui1;
#define GUI1 FindGUIID("GUI1")
import Label Label1;
import InvWindow InventoryWindow1;
import InvWindow InventoryWindow2;
import InvWindow InventoryWindow3;
import InventoryItem inventory[6];
import InventoryItem iInvItem1;
import InventoryItem iInvItem3;
import InventoryItem iInvItem2;
import InventoryItem iInvItem4;
import InventoryItem iBlueCup;
import InventoryItem iKey;
#define VIEW1 1

[close]

3. GlobalVariables.ash/asc - header/script pair containing declaration and initialization of variables created through the "Global Variables" panel in the editor.
Real output example under spoiler
Spoiler

Header:
Code: ags

import int IntVar;
import String StringVar;
import Button* SomeButton;
import DynamicSprite* MySprite;
import String StringVarEscapeT1;
import String StringVarEscapeT2;

Script:
Code: ags

int IntVar = 33;
export IntVar;
String StringVar;
export StringVar;
Button* SomeButton;
export SomeButton;
DynamicSprite* MySprite;
export MySprite;
String StringVarEscapeT1;
export StringVarEscapeT1;
String StringVarEscapeT2;
export StringVarEscapeT2;
function game_start() {
  StringVar = "This is a string";
  StringVarEscapeT1 = "String escape test: \\ \\ \\ ---";
  StringVarEscapeT2 = "String escape test: \" \" \" ---";
}

[close]

All the 3 above headers (not GlobalVariables.asc of course) are included to every single other script in the game.

Generated GlobalVariables.asc probably includes these 3 headers too, but nothing else.

4. Lastly there's DialogScripts.asc script, which contains all the dialog scripts converted to proper AGS script and merged together. This script includes every header from the game, just like the room scripts.

Snarky

Nice to know, thanks!

I'm a little curious how the Dialog.Start() command can delegate to a script lower down in the chain, but I assume it's either some kind of flag that causes it to run (possibly why calls such as these are not instantaneous, but only happen at the end of the script?), or the engine is doing something internally that isn't available in script.

Crimson Wizard

#8
Quote from: Snarky on Sun 23/05/2021 11:32:15
I'm a little curious how the Dialog.Start() command can delegate to a script lower down in the chain, but I assume it's either some kind of flag that causes it to run (possibly why calls such as these are not instantaneous, but only happen at the end of the script?), or the engine is doing something internally that isn't available in script.

Dialog.Start is not an equivalent to calling a script function. There's much more to the dialog than the script, its whole processing is wrapped by the engine. The dialog script is only run when the options are activated.

But yes, this is the reason why previous script function should end before dialog can begin, as engine does not support having another suspended script on the background.

SMF spam blocked by CleanTalk