Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - Crimson Wizard

#461
The big issue with the manual right now is that it has dedicated pages for editors and script commands, but does not have these for the basic features. You often have to go through all the "tutorials" or scripting section in order to find out that AGS can do something.

For the same reason, whenever I'd like to refer to how particular feature works in AGS (like speech, animation, dialog options), I cannot simply give a link to an article, and have to explain everything over again.

I opened a task ticket about it once: https://github.com/adventuregamestudio/ags-manual/issues/209
#462
The updated version, with some fixes related to game saves, and plugins which create their own objects:
https://cirrus-ci.com/task/5586565305466880
I made a comment about performance:
https://github.com/adventuregamestudio/ags/pull/1923#issuecomment-1519166283

In summary, it reduces slightly, but for me it was only noticeable in script-heavy games when running in "unlimited fps" mode. But I have not tested anything "heavy" that would have objects (cross-)referencing objects, simply because I don't have a ready game with that.

Still, some things in this branch were done as a quick first attempt, and there's room for improvement. Some optimization may be done quicker, other would require a redesign of e.g. managed object storage internally.



Quote from: eri0o on Sun 23/04/2023 23:13:05Hey, after applying this, would pursuing a refactor to port the script rewrite idea from Nick's branch much harder or the same difficulty?

No, I do not think that it makes it more difficult, as the main principles of the script executor's and managed pool's work were not changed.
The biggest changes are related to
* generating extra data (rtti) as a post-step in script compiling;
* loading extra data (rtti), and generating helper data from it;
* disposing user structs and dynamic arrays (changes are contained within Dispose method);
* garbage collector was practically correctly written now.
#463
Hmm... if I remember correctly, the speech is always displayed in relation to the first visible viewport/camera pair. So if your primary viewport is still on the screen, the it will be displayed in its coordinates.

EDIT: OR, the first viewport where character is visible.

Note that its considered to be "visible" even if there's another viewport overlaying it. You'd have to disable it by setting Screen.Viewport.Visible = false temporarily.

But, if you do not need original camera to be seen at this time, then you do not have to create new viewport/camera pair, and may adjust existing ones instead. They are accessible as Screen.Viewport and Game.Camera.

I recall this was an issue before because the manual had examples with unnecessary camera creations, so people copied them. These examples were changed since:
https://adventuregamestudio.github.io/ags-manual/Camera.html
https://adventuregamestudio.github.io/ags-manual/Viewport.html
#464
Engine Development / Re: Why is AGS slow?
Sat 22/04/2023 16:44:03
Quote from: Crimson Wizard on Thu 13/10/2022 22:05:15One major reason this was written in the first place was because AGS compiled script assumes 32-bit pointer size, so it won't work with 64-bit systems. Couple of people have suggested to implement a virtual memory instead, and use virtual 32-bit addresses instead of the real ones, which might fix this issue.
I suppose this is what Nick Sonneveld started to write in one of his experimental branches few years ago.

I've been testing a couple of script-heavy games with a Nick Sonneveld's script interpreter's rewrite
https://github.com/sonneveld/ags/commits/ags--script
using "infinite fps mode" (where the game runs as fast as possible without frame delays) and depending on a game and situation it gives about 20-25% improvement in fps, compared to the 3.5.0 engine it was based on. In one game it raised from 70 fps to around 84 fps, in another - from 330 to 400+ fps.

Code-wise it's bit dirty in places, and I don't know if it's fully feature complete.
There are few things that it probably does not do, which current engine does, like being able to address explicit variables from the engine structs exposed to script instead of letting interpreter read/write memory raw without knowing where it reads or writes to (which may be dangerous), but maybe it uses an alternate safety mechanism which I have not understood yet.

It also does very little safechecks, which is a very good thing for performance, but may make debugging for mistake harder. If it had these checks under some compilation flag, - that could improve debugging too.

Implementation-wise, it solves the memory issue by having a joint virtual/real memory mapper. Whenever possible the script data is allocated on the virtual memory "heap", which size is limited by 32-bit, which lets to reference it by using 32-bit offsets instead of real addresses. But when not possible (or not wanted for some reasons) it uses the virtual-to-real mem map (so it translates 32-bit handles to whatever-bit addresses). The latter is like the classic managed objects handles, except it seem to be able to work for anything. I haven't looked too deep into this, but I may imagine this mem map could be used for plugins too, which can allocate on their own and thus cannot be restricted to a virtual heap.

I actually wonder why we haven't tried at least this virtual-to-real map mechanism back in 2012/13, it alone might have been more performant than the solution that I did. It seems a quite logical thing to try.




Separately, I'd like to re-visit two my past comments in this thread:

Quote from: Crimson Wizard on Fri 24/09/2021 17:49:06Personally I would speculate that most games created with AGS so far may fit into 32-bit memory, and those which don't likely are overusing memory due to low optimization.

So, in the recent year it's been found that Dave Gilbert's new full-HD game actually goes above 32-bit RAM limit, but this was mostly due to the graphics. We did number of memory optimizations, which reduced the RAM usage by few hundreds MBs, but apparently reaching the limit is realistic. If this becomes a problem again, we might use 64-bit engine which has a much much more RAM support.


Quote from: Crimson Wizard on Fri 24/09/2021 17:49:06most of the mem is likely to be taken by resources (sprites, sounds), and these are not exposed into script VM, so not part of this address issue. What is left for VM addresses is: script variables and managed objects. Most managed objects are merely "wrappers" which contain ID of an actual object in the engine. So probably most managed script memory goes to: dynamic containers (arrays, etc), and dynamic sprites.

I must correct the last statement here: dynamic sprites do not store the image data in the script memory, it's being stored inside the sprite storage (aka sprite cache), and therefore this data does not have to be restricted by the size or address. The script's memory only stores minimal reference info.
#465
@glurex: you can use GetTranslation instead, that automatically returns the line in the current language:

Code: ags
player.SayBubble(GetTranslation("original text goes here"));

I think, this call to GetTranslation could be done right inside SayBubble. The problem here is that the message could be made using String.Format, and therefore not translatable. So it's a question of how to design the module...

Maybe there could be separate functions for automatically translated messages and not.

EDIT: you may write a helper function yourself:
Code: ags
void SayBubbleTra(this Character*, String message, GUI* bubbleGui)
{
  this.SayBubble(GetTranslation(message), bubbleGui);
}
and then use it like
Code: ags
player.SayBubbleTra("original text goes here");
#466
Quote from: Snarky on Fri 21/04/2023 17:33:28As for the rest, it depends on which character code is actually stored in the string at string manipulation time. If it is always LF, then things are simple and it is clear what to do.

AFAIK all the true escape sequences ('\\', '\n', '\r', '\t' and so forth) are dealt with at compilation time, and all the '\n' in the string you typed in the Editor will be LF in the compiled data.

What engine does at runtime is converting '[' into '\n'. This is where it tests for any backslashes before the '['.
This is done just before the string wrapping algorithm, so that the latter could work strictly with '\n's.

So, at the time when engine is drawing the line of text, it has to be either "\n" or "\\[". I suppose it's best to just replace everything with '\n' in script.
How to treat the "\\[" and "[\\" in otherwise reverse text case in script, - that is indeed open for interpretations...
One solution is to ignore these completely, and suggest users to use '\n' in their texts where they want a manual linebreak, because '\n' always becomes a single character. Then they will be dealt with by a compiler. Although, I don't know how it will be displayed if you type real Unicode text in RTL mode.
#467
Quote from: Snarky on Fri 21/04/2023 15:05:23One question: how will manual linebreaks made using "/n" appear in the reversed "RTL" string? As "/n", as "n/", or already converted to a newline code? (Actually, it might be best to optionally support all of these as well as the old ']', using some kind of configuration bit field.)

No, the line breaking chars do not need to be reversed, that makes no sense whatsoever. The `\n` (escaped 'n') is not treated as two characters, it's processed as a  single special character called LF (ascii code 10).
Converting '[' to ']' will only make sense if that's a displayed character, but won't if it's a special break character, in which case it must retain its code.
Both '\n' and '[' are treated by AGS during wrapped string drawing (I think it converts one to another for consistency, but I forgot the details); if they will be reversed, then nothing will work.
#468
Something has to write a script module for handling these special cases.

I think what is required is this:
* a function that calculates width of drawn text (given string, font and width limit) and inserts linebreaks in it.
* a special handling for the case when the text has to be read Right-to-left. Probably this means that either the splitting has to be done in reverse, or the text has to be reversed char by char twice: first before the splitting and then each separate part (between the linebreaks) is reversed on its own again.


For the reference, I had a function that splits text written long time ago for the TypedText module:
https://github.com/ivan-mogilko/ags-script-modules/blob/f55bc9015f6e6443de7f4d293b5b199779b79e88/scripts/gui/TypedText/TypedText.asc#L41
but it actually had a bug, mentioned here, with a proposed fix:
https://github.com/ivan-mogilko/ags-script-modules/issues/5

I had plans to pick this function out as a separate module, but never had found time to do this.

Or it could be rewritten from scratch.
#469
Alright, I wrote something, based on Python's idea:
https://github.com/adventuregamestudio/ags/pull/1923#issuecomment-1515510131
Test build:
https://cirrus-ci.com/task/6681544534786048

It seems working using the simple script test:
Spoiler
Code: ags
managed struct ListItem
{
    ListItem* prev;
    ListItem* next;
    
    String name;
    int data;
};

managed struct LinkedList
{
    ListItem* first;
    ListItem* last;
};

import ListItem* newItem(ListItem* add_after, String name, int data);
import void      detachItem(ListItem* item);
import ListItem* findNthItem(ListItem* first, int n);
import void      printItems(ListItem* first, ListBox* lb);

import ListItem* addItem(this LinkedList*, String name, int data);
import ListItem* remItem(this LinkedList*, ListItem* item);
import ListItem* remItemN(this LinkedList*, int index);
import void      clear(this LinkedList*);

Code: ags

ListItem* newItem(ListItem* add_after, String name, int data)
{
    ListItem* item = new ListItem;
    item.name = name;
    item.data = data;
    if (add_after != null)
    {
        ListItem* next = add_after.next;
        add_after.next = item;
        item.prev = add_after;
        if (next != null)
        {
            next.prev = item;
            item.next = next;
        }
    }
    return item;
}

void detachItem(ListItem* item)
{
    if (item.prev != null)
        item.prev.next = item.next;
    if (item.next != null)
        item.next.prev = item.prev;
    item.next = null;
    item.prev = null;
}

ListItem* findNthItem(ListItem* first, int n)
{
    ListItem* item = first;
    for (int i = 0; i < n; i++)
    {
        if (item.next == null)
            return null;
        item = item.next;
    }
    return item;
}

void printItems(ListItem* first, ListBox* lb)
{
    lb.Clear();
    if (first == null)
        return;
    ListItem* item = first;
    do
    {
        lb.AddItem(String.Format("%s : %d", item.name, item.data));
        item = item.next;
    }
    while (item != null);
}

ListItem* addItem(this LinkedList*, String name, int data)
{
    ListItem* item = newItem(this.last, TextBox1.Text, Random(32000));
    if (this.first == null)
        this.first = item;
    this.last = item;
    return item;
}

ListItem* remItem(this LinkedList*, ListItem* item)
{
    if (item == this.first)
        this.first = item.next;
    if (item == this.last)
        this.last = item.prev;
    detachItem(item);
    return item;
}

ListItem* remItemN(this LinkedList*, int index)
{
    ListItem* item = findNthItem(this.first, ListBox1.SelectedIndex);
    if (item == null)
        return null;
    this.remItem(item);
    return item;
}

void clear(this LinkedList*)
{
    this.first = null;
    this.last = null;
}
[close]
#470
The "Mark & Sweep" is probably the method which I described earlier, but, from what I understood, it requires to have a "root" reference which AGS does not have. This could be worked around by generating script reflection.

Comparing these two, I'd rather first try the one that does not require this.
#471
Alright, after reading this article, I can see that Python's gc has an opposite approach, where it does not check which pointers exist in script, instead it checks only the pointers in the "managed objects", to speak in AGS terms.

In a nutshell, there's a separate "reference counter" meant only for GC. It's initialized with real "reference counter" at the start. Then GC scans all the managed objects, and subrefs the objects it references, but it changes the "gc ref count" (not the real one). After that the objects that have 0 ref counts remaining are the ones that do not have link from the script memory.

Then it does a second pass, restoring "gc ref counts", but only starting at the objects that still kept positive "gc ref count" (these are ones that have a link from the script memory).

Finally in the end only the completely unattached objects remain with "gc ref count" = 0.

This is all really clever!, and this is possible to do in AGS without adding anything extra to script format, or checking internal engine refs; because this algorithm does not care about all that, it only cares to find out which refs come from withing the managed objects themselves.
#472
Quote from: eri0o on Tue 18/04/2023 22:27:49Not sure if it's because I am doing a lot of python recently, but the first result for garbage collection in Google for me was this: https://devguide.python.org/internals/garbage-collector/

Alright, I will read this, maybe this will give ideas how to optimize the whole thing...

Previously I found this funny article about how to write a GC memory in C language:
https://maplant.com/gc.html
#473
Quote from: Crimson Wizard on Tue 18/04/2023 20:27:50If the script memory would have contained hints on the data types in it, we could know where are "dynamic object handles" among it.

In regards to this: as the RTTI feature is already merged to ags4, it's potentially possible to expand and generate a script global variables table, which would have a structure similar to RTTI tables.

RTTI already has a Field type, which may as well be reused here.
It may contain even variable names, but that's not essential, and may be skipped for now.

To summarize, the necessary changes for GC would require:
1) compilers generating a reflection table for script's global variables (each script); the table is saved as another extension to script format, similar to how RTTI table is saved now.
2) writing a search algorithm that finds managed handles in the script memory.
3) figuring out a good trigger condition for running GC.


PS. I'd need to write a proper ticket for this; imo it's better to do this separately, and prior to the task of supporting nested managed pointers.
#474
Quote from: eri0o on Tue 18/04/2023 21:40:01I don't think I get quite well what circular dependency case you mean here, can you explain it with some example?

Simply when two objects reference each other, directly or indirectly.

Code: ags
// Example 1
struct LinkedItem {
    LinkedItem* next;
    LinkedItem* prev;
};

// Example 2
struct Parent {
    Child* children[];
};

struct Child {
    Parent* parent;
};



Quote from: eri0o on Tue 18/04/2023 21:40:01
QuoteSupposing we only run GC between the scripts, during the game frame update

Would the GC not need to happen on each scope change? Or would this only affect the circular dependent object?

What is "scope change"?

GC does not have a strict requirement to happen on particular events, it's okay to run it periodically.
It may be run by a timer, or under some trigger conditions based on managed pool statistics.

It also may be run on a separate thread, but only during some script-unrelated processes, like render or game update, that do not make any immediate script runs.
#475
Speaking theoretically, the GC should be able to detect, which managed objects are referenced from within the normal script memory, and which are not. If an object is not referenced from the script mem, - such object is detached and considered garbage.

NUANCE: engine may also hold "internal" managed references, so GC must also take that into account (not certain how yet).

Supposing we only run GC between the scripts, during the game frame update. That means that the only place we need to check is global script memory (because stack will be reset at the time). If the script memory would have contained hints on the data types in it, we could know where are "dynamic object handles" among it.

Hypothetically, the GC process then may work like this:

* scan the global script memory;
* for each value in that memory, which has a "dynamic handle" type,
   * mark that handle as used;
   * retrieve the dynamic object itself, find out if there are managed handles inside too, mark these as used;
   * repeat recursively for each connected dynamic object?

* after all this, compare list of existing handles with the list of "used" ones, and dispose objects with "unused" handles.
#476
Unfortunately, this is still not done. There's another problem which I forgot about: it's circular dependencies. The objects not referenced from script may still reference each other, which keeps them in memory until the program ends.

I never researched how this is done in other managed languages, but from a quick check they somehow are able to distinguish whether reference is from a script or internally from an object, and then consider every object that does not have a "script" reference a garbage amd remove it during garbage collection.

Need to invent something similar for AGS script now.

I really wish someone mentioned this earlier to me! Maybe it's going to be even larger work than reflection with RTTI... which means this functionality is not as close to completion as i supposed.
#477
I do not recommend doing things like "locname = Game.GetLocationName(mouse.x,mouse.y);" and "GetObjectAtScreenXY" in unhandled_event, because that is not exactly logical, as "unhandled_event" may be run for other reasons, not related to mouse clicks.

It's more reliable to remember the last clicked object etc in on_mouse_click, right before you run interaction over it, and save it in global variables.

EDIT: But maybe this is not important for your game, if it does not have such cases...

Also, only to mention this, you could avoid using unhandled_event at all, and make your own function(s) for handling this. Each interactable object has IsInteractionAvailable function, so you could even test this in on_mouse_click:
Code: ags
Object *obj = Object.GetAtScreenXY(mouse.x, mouse.y);
if (obj != null)
{
     if (obj.IsInteractionAvailable(mouse.Mode))
          obj.RunInteraction(mouse.Mode);
     else
          UnhandledObject(obj, mouse.Mode);
}

where "UnhandledObject" will be something like
Code: ags
function UnhandledObject(Object *obj, CursorMode mode)
{
}
#478
These events are normally fired when you interact with some object which does not have an interaction linked in. For instance, if you click "Look at", and the object does not have a "look at" reaction. The common use-case for them is to say something random, like "There's nothing of interest" or "I cannot use that".

QuoteI see that it jumps at any time when the action changes, not only on the initial click.

Could you elaborate, what do you mean by "action changes"?
#479
Also, not sure if relevant, but

Quote from: PERXEO on Sun 16/04/2023 20:37:21
Code: ags
function btnChangeMode_OnClick(GUIControl *control, MouseButton button)
{
  player.Say("CLICK TO CHANGE MODE");
  player.ActiveInventory=null;
  mouse.Mode=eModeUseinv;
  mouse.EnableMode(eModeUseinv);
  
}

Here you are setting mouse.Mode=eModeUseinv before enabling it, which makes no sense.
#480
If only 1 image is supposed to be on screen at the same time, then the easiest way here would be to have 1 GUI, and change its BackgroundGraphic. This way you always know if GUI with an image is visible, and which image is that.
SMF spam blocked by CleanTalk