Scripts and forward declerations?

Started by AnnIshman, Sun 14/08/2011 08:44:45

Previous topic - Next topic

AnnIshman

I've been trying to organize my code into some scripts and have run into some dependency problems. What I am trying to do is create some functions and enums that get used all over from the global and room scripts to other script files. Below is an example. In Script 1's header I define an enum that I use throughout the project, which works fine. If it try to use it in a function in script 2 that I want to import for the entire project, it seems I am not able to do so.

In this case I understand I could technically get away with an int to get it to work. When it comes to structs it is not as easily solved.

Script 1 Header
Code: ags

enum eA {
  a,
  b,
  c
};

import function foo(eA);


Script 2 Header
Code: ags

import function bar(eA);  // Error: PE03: Parse error at 'eA'


RickJ


monkey0506

To help clarify what Rick was saying, in the editor, does your list of scripts look like this:
|-Scripts
|--Script1.ash
|--Script1.asc
|--Script2.ash
|--Script2.asc
|--GlobalScript.ash
|--GlobalScript.asc
Or is Script2 listed above Script1? Script2 only has access to the scripts above it in the list (i.e., Script1), but not those below it (GlobalScript). You can move scripts up or down (right click on the script you want to move, GlobalScript will always be last in the list). Room (and dialog) scripts are considered to be below any of these scripts, so they have access to all of them.

It looks like you already understand how to import things properly, just make sure you have your scripts ordered correctly so that they can access the information they need. ;)

AnnIshman

Order was exactly my problem, thank you! Thanks for the resources.

It is a little tricky to keep circular dependencies out of the mix. Maybe you could offer a suggestion here. Say I want to notify any script that cares of something. A primitive event system seems in order, but I am having trouble putting something together that doesn't seem awful to manage in the long run.

Below is a simple example. The problem here is that Script 1 has to be at the bottom of the list and if Script 2 ever needs to call something in Script 1, which may be the sensible place to put something, you can't. As well it is messy to maintain notification lists like this, but not the worst thing in the world, I admit.

Script2.asc
Code: ags

// import in header.
function notifyScript2(int newValue) {
  foo(); // oh no! error, since it is in Script 1
}


Script3.asc
Code: ags

// import in header.
function notifyScript3(int newValue) {
  // Do things..
}


Script1.asc
Contains an accessor type function that notifies anyone who cares when it is updated
Code: ags

function setA(int value) {
  A = value; // A is a local variable to this script
  notifyScript2(A);
  notifyScript3(A);
  // etc..
}

// None of the other scripts can call me since I am in the bottom script.
function foo() {
   return A+B; // Does something particular to this script so can't be moved.
}

RickJ

#4
If it were me I would create an event module that would essentially consist of an event queue and the methods Event.Assert(event), Event.Occurred(event, remove=True), Event.InitQueue().

AGS doesn't support function pointers or any kind of callback mechanism.  So one script would place an event id in a queue and other scripts would have to poll, via repeatedly_execute(),  the event queue for any event(s) of interest.

[edit]
The only thing that comes close is CallRoomScript() (or someth9ing similar) that you may find useful.

Calin Leafshade

Forward declarations is definitely something that should be included in future versions of ags. Circular dependencies make it very hard to keep your scripts organised and modularised.

monkey0506

I realize you said that the foo function does something particular to Script1 so it can't be moved, but it might be possible to reorganize things if you could tell us more about what you're doing:

Code: ags
// Script0.asc
int A;
export A;
int B;
export B;

int foo()
{
  return A+B;
}


Code: ags
// Script2.ash

import void notifyScript2(int); // we don't have to specify parameter names in imports...

// Script2.asc

import int foo();

void notifyScript2(int newValue)
{
  foo();
}


Code: ags
// Script3.ash

import void notifyScript3(int newValue); // ...but we can if we want (just FYI)

// Script3.asc

void notifyScript3(int newValue)
{
  // ...
}


Code: ags
// Script1.asc

import int A;
import int B;
import int foo();

void setA(int value)
{
  A = value;
  notifyScript2(A);
  notifyScript3(A);
}


This would keep A, B, and foo out of the global scope, while giving access to the scripts as needed, by putting the imports into the script ASC file instead of the ASH header.

Something else that could help you out with this circular referencing problem would be to design some recursive functions. AGS only allows you to do somewhere around 14 levels of recursion, but it is allowed, and could be useful if you give it some meaningful parameters and then perform certain functions based on that...

Regarding adding forward declarations, the import keyword is essentially providing the necessary function here, the problem is simply that if you try and use the imported symbol before it's been defined that you get an "Already referenced name as import" error on compile. So it's not so much a matter of forward declarations that we need to look into, just forward linking. :P

Oh, and for managed structs, forward declarations already work fine. Internally AGS is doing something along the lines of this:

Code: ags
managed struct Button;
managed struct InvWindow;
managed struct Label;
managed struct ListBox;
managed struct Slider;
managed struct TextBox;

managed struct GUIControl
{
  import attribute Button* AsButton; // $AUTOCOMPLETENOINHERIT$
  import attribute InvWindow* AsInvWindow; // $AUTOCOMPLETENOINHERIT$
  // ...
};


Only valid (presently) for the managed types (which are all already defined anyway unless you're writing an engine plugin that uses new ones), but if we could ever get custom struct pointers then the capability would work the same (presumably). ;)

Knox

Without hijacking (or intending to), will global funtions be available in AGS aswell one day? As in, I can access a global funtion no matter where  it is?
--All that is necessary for evil to triumph is for good men to do nothing.

AnnIshman

I did not realize that import had scope although that makes perfect sense. I am unfortunately not able to try out your suggestions right now to really help them stick, but let me see if I understand the gist conceptually.

If you import in a header you have moved whatever it is you are importing into the global scope. Which may very well be the behavior you want, but then order is very important.

If you import instead in the ASC you keep that import local. In this case order is not important as everything will have been resolved already.


That last bit is a presumption on my part that is probably not correct as I see you put the foo declaration in the top script. After reading RickJ's suggestion I did start to move in this sort of direction of creating a sandwich of certain things in the top or bottom dependent on where they made sense. I'd really like to avoid polling with repeatedly_execute if possible, so I will have to experiment with all of this. Thanks again!

I will need to read up on managed structs as I haven't a clue what they are.


General_Knox:
If I understand your question correctly then you already can. Do a search in the help for "import" and "export" and I think that will get you to some good info.

Calin Leafshade

Quote from: General_Knox on Tue 16/08/2011 01:43:44
Without hijacking (or intending to), will global funtions be available in AGS aswell one day? As in, I can access a global funtion no matter where  it is?

If a function is in the first script in the list  (and imported into other script headers) then it is essentially global.

monkey0506

I think what Knox was asking is whether forward declaration (read as: forward linking :P) of functions is going to become available (which is what we were already talking about), but perhaps he just wasn't clearly understanding the terminology. And to be clear, functions don't get exported, only variables/pointers/struct instances/arrays do. ;)

As for the "managed" types, I hope I haven't caused any confusion by bringing them up, as they're not relevant to the thread. They're relevant to the idea of forward declarations (since this is supported by the struct keyword for use within other structs, which is currently only available to the built-in/managed types). I don't believe that the term "managed" is used anywhere in the manual in the context of the managed types, but as I said, you don't need to know much more about them than the fact that they are the built-in types.

The import keyword works the same regardless of whether it's in an ASH header or an ASC file. You can import a variable or function either before or after it has actually been defined, but you can't use that item until it has been fully defined. So the ordering of the scripts still matters. The difference between something being imported to global scope versus local scope has to do with the way that the ASH header files are compiled.

When you compile your game, every header file is copied into every subsequent script. So based on your most recent example (with the Scripts being ordered Script2, Script3, Script1, GlobalScript):

Script2.ash is copied into Script2.asc, Script3.asc, Script1.asc, GlobalScript.asc, and room and dialog scripts.
Script3.ash is copied into Script3.asc, Script1.asc, GlobalScript.asc, and room and dialog scripts.
Script1.ash is copied into Script1.asc, GlobalScript.asc, and room and dialog scripts.
GlobalScript.ash is copied into GlobalScript.asc, and room and dialog scripts.

None of the header files are copied up into higher scripts, only into the scripts that come after said header.

Something else that's important to note here is that when I say the headers are being "copied" what I mean is that they are being "copied". This is particularly important because it is perfectly valid for both Script2.asc and Script3.asc to have variables which are both defined "int foo" within their own respective local scopes (global within the entire script file, but not accessible outside of it). The two "foo" variables are completely unrelated.

So, if you define a variable inside of a script header, then you are essentially creating a new variable with that name for every script, and each of said variables are completely unrelated to the other variables (despite having the same name) in the other scripts. You're already aware of the import and export keywords of course, but I can't think of a single good reason to ever define a variable inside a script header, so to anyone reading this: DON'T DO IT!

Putting an import statement into the script header, as we've said, copies that import into each subsequent script. Then, that gives the subsequent scripts access to the single variable that you exported, so any changes or checks of the value will be persisted across all of your scripts (that have access to this variable).

Functions also should be imported and not defined inside of script headers, because in the latter case you would, again, be creating unique copies for every script file. What really makes that bad (beyond bloating the compiled game with a bunch of similarly named functions) is that if you change the function (inside of a header) then every script has to be recompiled. If you've got a hundred rooms in your game, that doesn't sound too optimal to have to recompile them all just because you added a comment to your function. The import route saves you the trouble there, so make sure you define functions inside of the ASC script files too. ;)

So that sums up how AGS handles the header files. As for what happens when you have an import inside of a script file, like I said, it works the same as when it's in a header, the difference is scope. Putting an import into a header file automatically gives that item global scope within every subsequent script. Putting an import into a script file, however, restricts the scope of that import only to that script file. NOTE: You cannot define an import inside of a room or dialog script.

This can be useful if you want (need) more than one script to have access to an item, but don't necessarily want it at the global scope of the entire project. In your particular case, the global scope may or may not be a problem for you, but you could perhaps use this method of getting around it if you needed to do so.

Another useful reason for imports inside of script files is so that you can define optional parameters:

Code: ags
// Script.asc

import int foo(int bar=0);

int foo(int bar)
{
  if (!bar)
  {
    // do this
  }
  else
  {
    // do that
  }
}


This would define a local function foo, and make its parameter bar optional, with a default value of 0. If you're not aware, it is possible to supply default parameters for any pointer type by substituting the integer value 0 in place of null (this will not work anywhere else, only in an import statement). Presently you cannot have optional float parameters, but you could work around that if needed by changing the parameter type to String and then using String.Format("%f", ...) and String.AsFloat to get the appropriate values, and for the case of a null String, supply the default value. It's a bit less friendly, but optional parameters were only ever designed to work with integral types, so the fact that they work with pointer types at all is a lucky happenstance.

So, hopefully this has been informative and not just a random information dump. :=

AnnIshman

Massively informative! Very many thanks. I would love to see more knowledge dumps like this of the inner workings of the engine for people like myself who are interested, but not able to invest the time to digging into the source to find these things out on their own.

So... How does 'forward linking' look as a potential enhancement?  ;D

This also clarifies exactly what is happening when you try to import the same object in multiple header files and why that causes a duplication problem. If I understand correctly you end up with something like this:

Code: ags

// Scrip1.ash
import function foo();


Code: ags

// Script2.ash
import function foo();


Code: ags

// Script3.ash
import function foo();



(Header copy step takes place in compilation)


Code: ags

// Script3.asc ends up looking like this:
import function foo();
import function foo(); // Error!
import function foo(); // Error!!

monkey0506

For the record, none of the "knowledge" that I "dumped" in my most recent post here came from the engine source (although I have perused through it somewhat), but rather just my years of experience in "I wonder what AGS would do if I...oh crap, it exploded again." ;D I've actually invested quite a lot of time in doing some very non-conventional things with AGS (which is subsequently how I've discovered things such as optional pointer parameters).

As for your example, it does indeed seem that you understand it correctly, although it would actually fail in Script2.asc due to the secondary import of foo. ;)

I haven't the time right now to look into the feasibility of this forward-linking thing, but perhaps someone else could care to take a look...?

SMF spam blocked by CleanTalk