Jibble

Author Topic: How would you work around AGS' strict order of modules dependencies?  (Read 584 times)

Monsieur OUXX

  • Mittens Vassal
  • Cavefish
  • Mittens Half Initiate
    • I can help with proof reading
    • I can help with translating
    • I can help with voice acting
    • Monsieur OUXX worked on one or more games that won an AGS Award!
    •  
    • Monsieur OUXX worked on one or more games that was nominated for an AGS Award!
Warning: This is advanced.
This is purely a question about programming in AGS language, to push its boundaries as far as possible. I'm exploring ways of implementing pseudo-callbacks in AGS script. For example, to be able to easily trigger stuff (in arbitrary locations of my scripts) whenever a Tween ends. But that's just one use case among thousands. And if I find a way I'd even go as far as implementing reflexivity.


Here's the scenario:

 - In most modern programing languages, you can declare the classes in any order and then call the methods of any class from the methods of any other class
 - Not in AGS, though. It works pretty much like C:
      You define a class "A" (actually a struct), that has a method "foo",
      then a struct "B", that has a method "bar",
      then inside B.bar() you may call A.foo(),
      but you cannot call B.bar() from inside A.foo(). Because the script file for A hasn't encountered B yet during compiling.
      => You need to think of the order of your dependencies properly.


However I do want to try to find a way of calling B.bar() from inside A.foo().
I see two ways of achieving this :

Idea #1 : you manage to implement an events pump as a hack for making dependencies loop

This one would only allow asynchronous calls (callbacks) because you can't predict when the event will be taken off the pile.
First you need to create an object to pile up so-called events, very low in the dependencies chain. So that every class can call it and say "I want to call a function please".
Then those events remain stored there until someone else decides to unpile them and start calling the requested functions.
I see two entities that could start reading the events from the pile :
 - Either I have a dormant piece of code in late_repeatedly_execute_always that does it and calls all my callbacks at the very end of the game loop.
OR
 - (Not sure if this one is technically possible) use AGS' built-in events to trigger a custom event. I'm just not sure when AGS would then read it, in the frame's lifecycle (in never use on_event because I suck). Then in on_event I would do the same thing as in the previous variant: read what function needs to be called, get the data (parameters and stuff) from the pile,  and call all the callbacks in an infinite loop until there are none left to call.

Idea #2 : Both are actually the same class, and the code is stored externally to AGS.

This is very hard. I'm not so much interested in that as  I am in idea #1. But this would allow an immediate, synchronous call.

Think Javascript. In a nutshell, their methods are not "real", they're just strings that you store in a dictionary when you initialize the game. Both A and B are actually some struct "Class" that has only one static method "Class.Call(structName, methodName, parameters)". Then, with more custom scripting, you let AGS run the relevant bit of code (the classe's method) from that, with nothing but a fancy giant "if (functionName=="foo") { ... }".

But then there's still the issue that the contents of the function (its instructions) have to be stored in an actual function, that's part of an actual module, and the dependencies order comes back (unless you want to write every functio of every class in the same struct, which is suicidal). So you'd need every so-called function to be accessible from anywhere. Basically, if the "events" hack described before is not enoug, then the only solution, I think, would be to write an AGS interpreter in AGS script and store the functions in external text files that would be read at runtime (there is already a primitive interpreter as part of module AGSConsole, or Ultravariables , that's a start).


=

So, what do you smart people think? Maybe focus on the on_event scenario for a start.
« Last Edit: 06 Mar 2020, 10:47 by Monsieur OUXX »
 

eri0o

My exploration with Custom Events: https://www.adventuregamestudio.co.uk/forums/index.php?topic=57065.0

I think - as in , I am not sure, so I may be wrong! - AGS has a limit of 4 "native", events per frame (on_key_press, on_mouse_click, on_event, ...), like, if more than four happen in the same frame, the others get dropped.

Monsieur OUXX

  • Mittens Vassal
  • Cavefish
  • Mittens Half Initiate
    • I can help with proof reading
    • I can help with translating
    • I can help with voice acting
    • Monsieur OUXX worked on one or more games that won an AGS Award!
    •  
    • Monsieur OUXX worked on one or more games that was nominated for an AGS Award!
AGS has a limit of 4 "native", events per frame (on_key_press, on_mouse_click, on_event, ...), like, if more than four happen in the same frame, the others get dropped.

No that's correct I've read it in the manual too.
 

Snarky

  • Global Moderator
  • Global Moderator
  • Mittens Lord
  • Private Insultant
    • Best Innovation Award Winner 2018, for his numerous additions to the AGS open source ecosystem including the new Awards Ceremony client and modules
    • Snarky worked on one or more games that won an AGS Award!
    •  
    • Snarky worked on one or more games that was nominated for an AGS Award!
I can't be of much help except to say I played around with something like this, and gave it up because I couldn't find an elegant way to do it. Lack of function pointers or any good way to simulate them is the big roadblock. As I recall, it's doable for individual functions, but you pretty much have to special-case it each time. I couldn't figure out a more general solution that would allow you to easily hook up new events/handlers, at least not without a preposterous amount of work.

My approach was basically to create a queue of generic "task" objects (at the top of the module stack), with fields specifying the function and its arguments, and another field for the return value. To "call" a function you would create a task, set the fields to specify the arguments, and add it to the queue (which would return a handle you could later use to retrieve the return value, inside a Wait() while-loop if you need it blocking). A repeatedly_execute_always() loop at the bottom of the module stack would churn through the task queue, interpret each entry and call the relevant function. But you have to add each callable function to the processing function manually, and handling different data types for the arguments and return values is very tedious (in order to get it general enough to cover a bunch of different functions you'd pretty much need to use a String serialization/deserialization, and at that point I decided to give up).

Spoiler: ShowHide
However, the awards client implements parts of this concept in two places:

1. It receives messages over IRC that are parsed and turned into commands, essentially through a big nested if-function (see AwardsProgramState and ReadDirectorData() in DirectorCommands).
2. In the nominees list (which I use to control the ceremony), the action that happens when you click on a list entry is defined in a similar command object (instances of Tree, which more accurately represent a tree node, and with the data stored as ints in Tree.data[]; the values in there are interpreted and carried out in DirectorConsoleDirectorConsole::RunCommand().


Usually I find that the best solution in AGS when you have "circular dependencies" between two structs is to split your struct B (and perhaps also struct A) in two: a more "primitive" struct B_Api that holds the basic data and API, which is placed higher in the module precedence so that struct A can call it, and a struct B_Manager (or perhaps better, AB_Manager)that holds more advanced "business logic," and is placed below struct A and able to call it (in your case, it would hold the "event loop").

So instead of:
-struct A
-struct B

With A.foo() unable to call B.bar(), you'd have:

-struct B_Api
-struct A_Api
-struct AB_Manager

With B.bar split into two function, B.bar1() that doesn't call A.foo(), and AB_Manager.bar2() which does. (Ugh, it would be clearer with a more concrete example.)

As the code develops, you may have to refactor it, perhaps several times, in order to achieve a clean separation.
« Last Edit: 07 Mar 2020, 11:07 by Snarky »

Monsieur OUXX

  • Mittens Vassal
  • Cavefish
  • Mittens Half Initiate
    • I can help with proof reading
    • I can help with translating
    • I can help with voice acting
    • Monsieur OUXX worked on one or more games that won an AGS Award!
    •  
    • Monsieur OUXX worked on one or more games that was nominated for an AGS Award!
Don't worry, it's crystal clear.
Yes, I'm afraid there's no magical solution to break the depencies cycle.
I can see one possible enhancement (I won't detail here) to let A and B route themselves which of their methods is called by the event at a higher level. They would contain their own "switch case" where every case calls directly a helper function that's just right next to it inside A or B,. This way the events "depiler" doesn't have to know anything about A or B except their existence.

 Also maybe add classes AB here and there in the chain of depencies for doing the same kind of routing, grouped by classes themes (some sort of degenerate system of namespaces)

Long things short: it's kinda possible, but as you said it requires a ton of work.

That said, the new Dictionary object alleviates the whole serialise/deserialize madhouse and object pointers. I wish there was the same kind of object but for Arrays of int (Crimson A and the gang wink wink this is my monthly whining about that feature).

The one thing that bums me out, though, is still the same thing: the absence of control on WHEN the stacked events get unstacked. Gurrr.
« Last Edit: 07 Mar 2020, 13:57 by Monsieur OUXX »
 

I can see one possible enhancement (I won't detail here) to let A and B route themselves which of their methods is called by the event at a higher level. They would contain their own "switch case" where every case calls directly a helper function that's just right next to it inside A or B,. This way the events "depiler" doesn't have to know anything about A or B except their existence.


Idk if it's the same, in the past I was working on a scheduled commands systems where user (e.g. room script) could fill in a list of actions, and they would execute one after another. In fact, I was refactoring existing system, because wanted to split it into several modules and allow to add more modules to extend it. Where each module would implement its own set of commands.

So what I did was this:

+ Command queue controller
+ Command implementor 1
+ Command implementor 2
+ ...
+ Global Script, and room scripts

Command queue module would do the generic work: check current states, timers, and activate next command, and so on. But it's the modules below in list that would do actual work.
In this scheme the queue controller did not actually know anything about actual implementing modules (and I thought that's correct). But that also meant that it cannot call them directly to start a new command.

So what I did was this: I exported a number of properties describing "current command" in the controller, and let the implementing modules check it in rep-exec.
They checked it in an order, obviously, and the one that found a "known" command would claim it, cleaning up some flag in the queue properties.

It worked (very pseudocode) like this:
Spoiler: ShowHide

Code: Adventure Game Studio
  1.  
  2. struct QueueController
  3. {
  4.       CommandDescription* NextCommand;
  5.      
  6.       import void ClaimNextCommand(); // claim and flag next command as being executed by something
  7. };
  8.  
  9.  
  10. struct BasicExecutor {...};
  11. struct CharacterExecutor {...};
  12.  
  13. void CharacterExecutor::Update()
  14. {
  15.      if (queue.NextCommand.Type == eCharCommand) {
  16.           CommandDescription* desc = queue.NextCommand;
  17.           queue.ClaimNextCommand();
  18.           Execute(desc);
  19.      }
  20. }
  21.  
  22.  




That said, the new Dictionary object alleviates the whole serialise/deserialize madhouse and object pointers. I wish there was the same kind of object but for Arrays of int (Crimson A and the gang wink wink this is my monthly whining about that feature).

What kind of object for "arrays of int"?
« Last Edit: 07 Mar 2020, 21:30 by Crimson Wizard »

eri0o

Hey CW, I love the command queue!

I was looking into a similar system myself for some simple dialogs, I was playing around with the idea of navigating a dialog and enabling the player to go back, but I also wanted to be easy for me laying out the tree in a way that was as easy as it's currently with blocking functions like player.Say(), player.Walk() and stuff like that.

The idea was to implement a "video/memory" scene that could go forward and backward but you could pause to look at things similar to Remember Me memories (I linked one below).
Spoiler: ShowHide


I never thought of breaking the thing into a module because the particular dialog was linked to a GUI with label and buttons that showed the conversation and allowed going forward and backward. I also gave up on it because the 2D nature of it didn't make it quite compelling. Still, I used a small cutdown version of it (without going backwards) to make some interfaces on my game that show story bits, but that you can still show the game menu on top - you know, in case you want to quit the game, load a save or change a config while something else is "going".

I think this may be different from what Monsieur OUXX is talking - or maybe not, I am still quite confused by what exactly is being looked into here.
« Last Edit: 09 Mar 2020, 01:10 by eri0o »

Monsieur OUXX

  • Mittens Vassal
  • Cavefish
  • Mittens Half Initiate
    • I can help with proof reading
    • I can help with translating
    • I can help with voice acting
    • Monsieur OUXX worked on one or more games that won an AGS Award!
    •  
    • Monsieur OUXX worked on one or more games that was nominated for an AGS Award!
What kind of object for "arrays of int"?

Code: Adventure Game Studio
  1. IntArray* a = IntArray.Create();
  2. a.Insert(0, 666); // a == [666]
  3. a.Insert(0, 777); // a == [777, 666]
  4. Display ("%d", a.Length); // "2"
  5. a.Set(0, 888); // a == [888, 666]
  6. a.Remove(0); // a == [666]
  7.  
  8. // If you have the energy :
  9. a.Sort();
  10.  
  11. // If you have the energy :
  12. FloatArray* a = FloatArray.Create(); //If you do that, you divide the size of the Tweens module's script by 3. :-D
  13.  

The main difference with int[] being that you can pass the object to functions (without having to return the new pointer to it if you increased its size inside the function), create an array of it (arrays of arrays: the holy grail), and get extra attributes (like Length) without hacky programming.
At the moment I'm doing it using the Dictionary but all the String to int conversion is so slow.
« Last Edit: 09 Mar 2020, 13:20 by Monsieur OUXX »
 

The main difference with int[] being that you can pass the object to functions (without having to return the new pointer to it if you increased its size inside the function), create an array of it (arrays of arrays: the holy grail), and get extra attributes (like Length) without hacky programming.
At the moment I'm doing it using the Dictionary but all the String to int conversion is so slow.

I think most of this may be done in script. You can write a function that resizes dynamic array by creating a new one and copying old into it, and you can store length of array as the first (0) index for simple types (for complex types this becomes less convenient).
You may also save "capacity" in the array header, which lets you to insert and remove elements without fully reallocating an array.

I had the module that did this for array of ints, may post the code when I get to my home PC.


As for having such script class, the most irritating problem with AGS script is that it does not support generic/template syntax, which means that you would have to declare separate array type for each builtin type. And there's won't  be a way to declare such array object for user types. With Dictionary I made a big compromise.
« Last Edit: 09 Mar 2020, 14:09 by Crimson Wizard »

Probably completely offtopic, but since I promised above, here's the array module.
Unfortunately it was in the midst of writing, so it may be not fully tested and even not 100% working, but this is meant to give a basic idea:

Header -
Spoiler: ShowHide

Code: Adventure Game Studio
  1. // Array is open source under the MIT License.
  2. //
  3. // TERMS OF USE - Array MODULE
  4. //
  5. // Copyright (c) 2019 Ivan Mogilko
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy of
  8. // this software and associated documentation files (the "Software"), to deal in
  9. // the Software without restriction, including without limitation the rights to
  10. // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  11. // the Software, and to permit persons to whom the Software is furnished to do so,
  12. // subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in all
  15. // copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  19. // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  20. // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  21. // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  22. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. //
  24. //////////////////////////////////////////////////////////////////////////////////////////
  25. //
  26. // Module implements various operations over dynamic array of integers.
  27. //
  28. //////////////////////////////////////////////////////////////////////////////////////////
  29.  
  30. #ifndef __ARRAYCLASS_MODULE__
  31. #define __ARRAYCLASS_MODULE__
  32.  
  33. #define ARRAYCLASS_VERSION_00_00_90_00
  34.  
  35. #define ARRAY_HEADER_SIZE        3
  36. #define ARRAY_HDRI_CAP           0
  37. #define ARRAY_HDRI_ELSZ          1
  38. #define ARRAY_HDRI_CNT           2
  39.  
  40. // Copies only unique set of items, compare by field at id_offset
  41. import int[] Array_CopyUnique(int arr[], int begin, int end, int id_offset = 0);
  42. // Copies only unique set of items, compare by field at id_offset
  43. import int[] Array_CopyUniqueAll(int arr[], int id_offset = 0);
  44. // Copies full array
  45. import int[] Array_Copy(int arr[]);
  46. // Shrinks if capacity is larger than item count
  47. import int[] Array_ShrinkIf(int arr[]);
  48. // Finds element looking for keys at id_offset, returns slot
  49. import int Array_Find(int arr[], int what, int begin, int end, int id_offset = 0);
  50. // Finds element looking for keys at id_offset, returns slot
  51. import int Array_FindAnywhere(int arr[], int what, int id_offset = 0);
  52. // Allocates array with meta data, reserves space for some items
  53. import int[] Array_New(int elem_size, int reserve_count);
  54. // Reallocate array's copy of greater size if need_count is larger than fits in capacity
  55. import int[] Array_ExpandIf(int arr[], int need_count);
  56. // Erases certain item and shifts items to close the gap
  57. import void Array_EraseItem(int arr[], int index);
  58.  
  59. #endif // __ARRAYCLASS_MODULE__
  60.  


Body:
Spoiler: ShowHide

Code: Adventure Game Studio
  1.  
  2. int[] Array_CopyUnique(int arr[], int begin, int end, int id_offset)
  3. {
  4.   int arr_elsize = arr[ARRAY_HDRI_ELSZ];
  5.   int item_count = (end - begin) / arr_elsize;
  6.   int capacity = item_count * arr_elsize + ARRAY_HEADER_SIZE;
  7.   int buf[] = new int[capacity];
  8.   buf[ARRAY_HDRI_CAP] = capacity;
  9.   buf[ARRAY_HDRI_ELSZ] = arr_elsize;
  10.   int added = 0;
  11.   int added_size = 0;
  12.  
  13.   for (int i = begin; i < end;)
  14.   {
  15.     int item = arr[i + id_offset];
  16.     bool found = false;
  17.     for (int j = 0; j < added_size; j += arr_elsize)
  18.     {
  19.       if (buf[j] == item)
  20.       {
  21.         found = true;
  22.         break;
  23.       }
  24.     }
  25.  
  26.     if (found)
  27.     {
  28.       i += arr_elsize;
  29.     }
  30.     else
  31.     {
  32.       for (int c = added_size; c < added_size + arr_elsize; c++)
  33.       {
  34.         buf[c] = arr[i];
  35.         i++;
  36.       }
  37.       added++;
  38.       added_size += arr_elsize;
  39.     }
  40.   }
  41.   buf[ARRAY_HDRI_CNT] = added;
  42.   return buf;
  43. }
  44.  
  45. int[] Array_CopyUniqueAll(int arr[], int id_offset)
  46. {
  47.   int arr_elsize = arr[ARRAY_HDRI_ELSZ];
  48.   int arr_count = arr[ARRAY_HDRI_CNT];
  49.   return Array_CopyUnique(arr, ARRAY_HEADER_SIZE, ARRAY_HEADER_SIZE + arr_count * arr_elsize, id_offset);
  50. }
  51.  
  52. int[] Array_Copy(int arr[])
  53. {
  54.   int arr_capacity = arr[ARRAY_HDRI_CAP];
  55.   int arr_elsize = arr[ARRAY_HDRI_ELSZ];
  56.   int arr_count = arr[ARRAY_HDRI_CNT];
  57.   int took_space = ARRAY_HEADER_SIZE + arr_count * arr_elsize;
  58.   int new_arr[] = new int[took_space];
  59.   for (int i = ARRAY_HEADER_SIZE; i < took_space; i++)
  60.   {
  61.     new_arr[i] = arr[i];
  62.   }
  63.   new_arr[ARRAY_HDRI_CAP] = took_space;
  64.   new_arr[ARRAY_HDRI_ELSZ] = arr_elsize;
  65.   new_arr[ARRAY_HDRI_CNT] = arr_count;
  66.   return new_arr;
  67. }
  68.  
  69. int[] Array_ShrinkIf(int arr[])
  70. {
  71.   int arr_capacity = arr[ARRAY_HDRI_CAP];
  72.   int arr_elsize = arr[ARRAY_HDRI_ELSZ];
  73.   int arr_count = arr[ARRAY_HDRI_CNT];
  74.   int took_space = ARRAY_HEADER_SIZE + arr_count * arr_elsize;
  75.   if (took_space == arr_capacity) return arr;
  76.   int new_arr[] = new int[took_space];
  77.   for (int i = ARRAY_HEADER_SIZE; i < took_space; i++)
  78.   {
  79.     new_arr[i] = arr[i];
  80.   }
  81.   new_arr[ARRAY_HDRI_CAP] = took_space;
  82.   new_arr[ARRAY_HDRI_ELSZ] = arr_elsize;
  83.   new_arr[ARRAY_HDRI_CNT] = arr_count;
  84.   return new_arr;
  85. }
  86.  
  87. int Array_Find(int arr[], int what, int begin, int end, int id_offset)
  88. {
  89.   int arr_elsize = arr[ARRAY_HDRI_ELSZ];
  90.   for (int i = begin; i < end; i += arr_elsize)
  91.   {
  92.     if (arr[i + id_offset] == what) return i;
  93.   }
  94.   return -1;
  95. }
  96.  
  97. int Array_FindAnywhere(int arr[], int what, int id_offset)
  98. {
  99.   int arr_elsize = arr[ARRAY_HDRI_ELSZ];
  100.   int arr_count = arr[ARRAY_HDRI_CNT];
  101.   int end_slot = ARRAY_HEADER_SIZE + arr_elsize * arr_count;
  102.   return Array_Find(arr, what, ARRAY_HEADER_SIZE, end_slot, id_offset);
  103. }
  104.  
  105. int[] Array_New(int elem_size, int reserve_count)
  106. {
  107.   int capacity = ARRAY_HEADER_SIZE + elem_size * reserve_count;
  108.   int arr[] = new int[capacity];
  109.   arr[ARRAY_HDRI_CAP] = capacity;
  110.   arr[ARRAY_HDRI_ELSZ] = elem_size;
  111.   return arr;
  112. }
  113.  
  114. int[] Array_ExpandIf(int arr[], int need_count)
  115. {
  116.   int arr_capacity = arr[ARRAY_HDRI_CAP];
  117.   int arr_elsize = arr[ARRAY_HDRI_ELSZ];
  118.   int arr_count = arr[ARRAY_HDRI_CNT];
  119.   int want_space = ARRAY_HEADER_SIZE + need_count * arr_elsize;
  120.   if (want_space <= arr_capacity) return arr;
  121.  
  122.   int new_arr[] = new int[want_space];
  123.   int took_space = ARRAY_HEADER_SIZE + arr_count * arr_elsize;
  124.   for (int i = ARRAY_HEADER_SIZE; i < took_space; i++)
  125.   {
  126.     new_arr[i] = arr[i];
  127.   }
  128.   new_arr[ARRAY_HDRI_CAP] = want_space;
  129.   new_arr[ARRAY_HDRI_ELSZ] = arr_elsize;
  130.   new_arr[ARRAY_HDRI_CNT] = arr_count;
  131.   return arr;
  132. }
  133.  
  134. void Array_EraseItem(int arr[], int index)
  135. {
  136.   int arr_elsize = arr[ARRAY_HDRI_ELSZ];
  137.   int arr_count = arr[ARRAY_HDRI_CNT];
  138.   if (index < 0 || index >= arr_count) return;
  139.   int last_slot = ARRAY_HEADER_SIZE + arr_elsize * (arr_count - 1);
  140.   for (int slot = ARRAY_HEADER_SIZE + arr_elsize * index; slot < last_slot; slot++)
  141.   {
  142.     arr[slot] = arr[slot + arr_elsize];
  143.   }
  144.   arr[ARRAY_HDRI_CNT] -= 1;
  145. }
  146.  


In brief, what's going on there:

- The module is meant to work with a special case of array of ints, where first N elements are actually a "meta data" (size of ARRAY_HEADER_SIZE). Every function in the module assumes that passed array is compatible with that rule.
- Meta data (array header) stores: array capacity (real length of memory), element size, in ints (custom, 1 by default), and number of valid elements. Thus the array works as a classic "vector" type, where number of valid elements may be less than the capacity, and some space is kept reserved for later additions.

The biggest caveat with this is that you have to always remember not to do standard "for (int i = 0; i < len; i++) arr[ i ]" with it, as well as you cannot do "arr[ i ]" to get the ith element. Instead, the element is addressed as "arr[meta_size + i * el_size]", or at least "arr[meta_size + i]" in the simpliest case.

If you manage to live with the above, then you will have practically a C++ vector, that you may reallocate, copy, resize, search through, and add any other algorithms of your own.
« Last Edit: 09 Mar 2020, 19:38 by Crimson Wizard »

Monsieur OUXX

  • Mittens Vassal
  • Cavefish
  • Mittens Half Initiate
    • I can help with proof reading
    • I can help with translating
    • I can help with voice acting
    • Monsieur OUXX worked on one or more games that won an AGS Award!
    •  
    • Monsieur OUXX worked on one or more games that was nominated for an AGS Award!
I think most of this may be done in script.
No. It's the answer that's given every time, and no it can't be done in script
Again : You can't do dynamic arrays of arrays in AGS, and when you pass an int[] you have to pass its length (or save it as metadata and then you ruin your straightforward for loops like you said) and return the int[] if you changed it, it's a nightmare. I have the exact same Array module as you have, and both our modules do exactly that : pass the length and return the new int[]. If you want to implement something as simple as Pop (which must return the resized array and the value) you hit a wall and the hacky solutions start.

If you go that way, Dictionary and Set could also have been done in script.
I don't want to throw tantrum, it's just that I have to same the same things every time for 10 years. I can even continue the conversation on my own : "But of course you can do arrays of arrays, if you store a two-dimensional array into a regular array", at which point I reply that then the biggest array determines the size of all arrays (unpractical for hundreds of arrays of wildly different sizes) or you have to implement crazy-complicated memory allocation routines.
« Last Edit: 10 Mar 2020, 08:20 by Monsieur OUXX »
 

No. It's the answer that's given every time, and no it can't be done in script

But you say this, and then give examples of how this could be done. So, apparently, it can be done, but requires an effort.
In the first post of this thread you said that you are up to "push its (AGS Script) boundaries as far as possible". So, this is it.

My belief is, if it's about arrays of int, or structs of data described by multiple ints, with certain effort and systematic approach, all of that can be done in script.
IMHO it's mostly about writing enough helper functions and learning a pattern to follow when you work with these.

This could be done for floats too, or combined ints/floats, although in that case you'd have to do constant IntToFloat and FloatToInt, which sucks.


If you want to implement something as simple as Pop (which must return the resized array and the value) you hit a wall and the hacky solutions start.

You don't have to return the resized array in Pop, you don't reallocate the array with Pop, you just change the count info and/or first offset info inside same array.
This is about how e.g. vectors work in C++.

The quick example, based on the previous script:
Spoiler: ShowHide

Code: Adventure Game Studio
  1. int Array_Pop(int arr[])
  2. {
  3.     int count = arr[COUNT_POS];
  4.     int value = arr[count - 1];
  5.     arr[COUNT_POS] -= 1;
  6.     return value;
  7. }
  8.  


In the worst case, the operation is split in two sequential calls (this is how C programming looks like sometimes...).
Indeed that is not as convenient as having one function that does everything, but I would not call this a "nightmare", that's rather a lower level programming.


I can even continue the conversation on my own : "But of course you can do arrays of arrays, if you store a two-dimensional array into a regular array", at which point I reply that then the biggest array determines the size of all arrays (unpractical for hundreds of arrays of wildly different sizes) or you have to implement crazy-complicated memory allocation routines.

You could similarily store multiple array metadata in the beginning of a big array.
The metadata may look like this:
- number of arrays
- list of offsets and sizes per every "allocation"
- then follows each array, with it's own metadata (as described in the above example).

The complication there would be that the array functions that read or adjust array would have to pass "offset" along with the array[] pointer.

To read the sub-array of index N, you'd have to do, e.g.:
Code: Adventure Game Studio
  1. int arr_off = multiarr[1 + N * 2];
  2. int arr_len = multiarr[1 + N * 2 + 1];
  3. for (int i = arr_off; i < arr_off + arr_len; i++)
  4. ...
  5.  

Of course, it makes things objectively harder, but I would not call this "crazy-complicated".


EDIT: The bigger problem with this is, of course, that this may be too time costly if arrays increase sizes often. Such structure will be usable only so long as sub-arrays either don't increase sizes, or increase them in a limited range (or limited times). As with single array, decrease in size is not a matter of concern, because you do not have to reallocate array each time an element is removed, only adjust its meta-data.



I must admit that this is not something I'd normally favor doing myself, but if I were stuck making games with the same engine for 10+ years, then I would definitely do.
« Last Edit: 11 Mar 2020, 08:43 by Crimson Wizard »

Snarky

  • Global Moderator
  • Global Moderator
  • Mittens Lord
  • Private Insultant
    • Best Innovation Award Winner 2018, for his numerous additions to the AGS open source ecosystem including the new Awards Ceremony client and modules
    • Snarky worked on one or more games that won an AGS Award!
    •  
    • Snarky worked on one or more games that was nominated for an AGS Award!
All I know is that if I need to have an array of strings in a managed struct, or anything along those lines, it becomes a complete nightmare of referencing external tables through index arithmetic, "assigning" and "freeing" sections of table space, secondary lookup tables, etc. What would be one line in most C-like languages is sometimes literally days of work to implement.

All I know is that if I need to have an array of strings in a managed struct, or anything along those lines, it becomes a complete nightmare of referencing external tables through index arithmetic, "assigning" and "freeing" sections of table space, secondary lookup tables, etc. What would be one line in most C-like languages is sometimes literally days of work to implement.

That is a problem of compiler not saving the type description for the engine, and because of that engine cannot know which elements of struct are managed pointers, and cannot decrease reference counts. Objects stay unreleased, thus accumulating "memory leak". This is why we had to disable placing pointers into manage structs, but syntactically it was already supported and proved working.

No "array" class would fix that, for instance you similarily won't be able to have the "array" object in the managed struct, as you now cannot put Dictionary in the managed struct. It has to be a proper script feature, that would allow any kind of connections between managed objects and cover limitless cases at once.

The solution was discussed numerous times in the past, it may not be too complex from what I remember, only requiring good effort, but no one set on implementing this.
« Last Edit: 10 Mar 2020, 19:39 by Crimson Wizard »