Custom Event

Started by Dualnames, Tue 02/04/2019 02:56:10

Previous topic - Next topic

Dualnames

I'm trying to remember how this works, I remember vividly a module that had like 2 extra custom events appended to the on_event function.
Sorry for the creating of many topics lately <3

It's important that I'd have that, cause it would help me avoid 'working around' which means a lot of effort and code.
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

Crimson Wizard

I have what may be a nice workaround, but never tested this myself.

You could reuse some of the built-in events and convert event's argument into custom event type.

For example, eEventGotScore may be less used event type. Call it passing your custom event id as a "score amount":
Code: ags

function CallCustomEvent(int custom_code)
{
     GiveScore(custom_code);
}


and then
Code: ags

function on_event (EventType event, int data) 
{
     if (event == eEventGotScore)
         event = 100 + data; // convert to custom event id
}


you could also probably use eEventAddInventory event for this, if you've got a room for dummy items that would mean custom events. Inventory items also have custom properties, which may let you pass all kinds of "event arguments" this way. E.g. you may spare 1 dummy item for this, and setup its properties to configure type of event you are sending.

Dualnames

How come i didn't think of the score event roflmao, yeah, that's a fantastic idea!
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

eri0o

#3
Hey, I think the code below may work, as simple complication of the CW idea! Add it at top of your scripts to be able to use events to ignore script order dependency to call and listen events.

CustomEvent.asc
Code: ags

Dictionary* events;
Dictionary* id_to_event_map;
int eventIDs;

void emitCustomEvent(String CustomEvent){
  if(events==null){
    events = Dictionary.Create(); 
    id_to_event_map = Dictionary.Create(); 
  }
  
  if(!events.Contains(CustomEvent)){  
    events.Set(CustomEvent, String.Format("%d",eventIDs));
    id_to_event_map.Set(String.Format("%d",eventIDs), CustomEvent);
    eventIDs++;
  }
  
  String event_id_asString = events.Get(CustomEvent);
  
  GiveScore(100+event_id_asString.AsInt);
}

bool listenCustomEvent(EventType event, int data, String CustomEvent){
  if(event==eEventGotScore && data>= 100){
    int event_id = data - 100;
    
    if(id_to_event_map.Get(String.Format("%d",event_id)) == CustomEvent){
      return true;
    }
  }
  return false;
}


CustomEvent.ash
Code: ags

import void emitCustomEvent(String CustomEvent);
import bool listenCustomEvent(EventType event, int data, String CustomEvent);


To emit custom events

Code: ags

emitCustomEvent("MyCustomEvent");



To listen to custom events

Code: ags

void on_event(EventType event, int data){
  if(  listenCustomEvent( event, data, "MyCustomEvent") ){
    // do something
  }
}

Crimson Wizard

eri0o, if you are making custom events map anyway, then you do not really need "on_event" and eEventGotScore, you may as well listen in repeatedly_execute and check whether events queue is not empty.

eri0o

That's a good point CW. This is the code I ended up using.

CustomEvent.ash
Code: ags
// Custom Event Module Header
// Add enums as needed for your events
enum CustomEventType {
  eCET_DialogStarted,
  eCET_DialogEnded,
}; 

import void emitCustomEvent(CustomEventType customEvent); 
import bool listenCustomEvent(EventType event, int data, CustomEventType customEvent);


CustomEvent.asc
Code: ags
// Custom Event Module Script

void emitCustomEvent(CustomEventType customEvent){ 
  GiveScore(100+customEvent); 
} 

bool listenCustomEvent(EventType event, int data, CustomEventType customEvent){
   if(event==eEventGotScore && data>=100){ 
     if(data-100 == customEvent){
       return true; 
     } 
   } 
   
  return false; 
}

Khris

I must be missing something because I don't see how any of this is an improvement over just calling a regular old function?

Crimson Wizard

Quote from: Khris on Tue 16/07/2019 13:46:13
I must be missing something because I don't see how any of this is an improvement over just calling a regular old function?

IDK what's the original posters' intentions were, but in theory
1) this lets to connect independent modules that do not know about each other. If you add new module it already may reuse same events without modifying any existing code.
2) indirectly call function in module located below
3) maybe implement custom "claimable event" similar to built-in ClaimEvent function when the first module that processes event can claim it, preventing passing it to others.

eri0o

#8
Yes, overall, I use for ignoring script ordering, and I think emiting and consuming events are good for things that don't happen often, instead of having to check conditions on repeatedly execute. They feel more organized too, for some cases.

Dualnames

My intention was to be able to use on_event on several of my modules for my savegame system, but honestly this is a savesystem I had to code while the project is/was at the time more than halfway done, so it was harder to do it properly, to be fair I've already gotten an answer from you, CW, but I like that eri0o is discussing it, I think it's an interesting topic.
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

eri0o

I will add a failed attempt at event listener here just because I like this topic...

So, I know we can't have function pointers right now in AGS, but I do know struct pointers exist... And we can extend structs. You may already guessed now what was my mistake, since I too thought it wouldn't work, still, I had to try it.

EventModule.ash , the header.
Code: ags
// event module header

/// an empty custom listener to be extended and have act() overloaded
managed struct CustomListener {};

/// subscribe a new listener to an event
import void AddEventListener(String eventName, CustomListener* listener);

/// emit an event
import bool emit(String eventName);


EventModule.asc , the script, this is ugly, so I will hide with spoiler.
Spoiler
Code: ags
// new module script
#define MAX_EVENT_COUNT 32
#define MAX_EVENT_TYPES 64
#define MAX_LISTENER_COUNT 16
readonly int _max_events = MAX_EVENT_COUNT;
readonly int _max_listeners = MAX_LISTENER_COUNT;

String _event_pipe[MAX_EVENT_COUNT];

struct Event_Listener{
  int ListenerCount;
  CustomListener* Listeners[MAX_LISTENER_COUNT];
};
Dictionary* _event_map;
int _event_ids;

Event_Listener _event_listeners[MAX_EVENT_TYPES];
int _event_count;

//to be overloaded later on an extended struct
function act(this CustomListener*) {
  Display("undefined CustomListener acted"); 
}

bool emit(String eventName){
  if(_event_count>=_max_events){
    return false;
  }
  
  _event_pipe[_event_count] = eventName;
  _event_count++;
  return true;
}

void AddEventListener(String eventName, CustomListener* listener){
  if(_event_map==null) _event_map = Dictionary.Create();
  
  if(!_event_map.Contains(eventName)){  
    _event_map.Set(eventName, String.Format("%d",_event_ids));
    _event_ids++;
  }
  
  String event_id_asString = _event_map.Get(eventName);
  
  int listenerCount = _event_listeners[event_id_asString.AsInt].ListenerCount;
  _event_listeners[event_id_asString.AsInt].Listeners[listenerCount] = listener;
  _event_listeners[event_id_asString.AsInt].ListenerCount++;
}

void ProccessEvents(){
  while(_event_count>0){
    _event_count--;
    String event_id_asString = _event_map.Get(_event_pipe[_event_count]);
        
    int i=0;
    while( i < _event_listeners[event_id_asString.AsInt].ListenerCount){
      _event_listeners[event_id_asString.AsInt].Listeners[i].act();
      i++;
    }    
  }  
}

void repeatedly_execute(){
  ProccessEvents();
}
[close]

The usage, on a room1.asc script.
Code: ags
// room script file
managed struct BurnListener extends CustomListener{};
void act(this BurnListener*) {
  Display("Burn with fire");
}

function room_Load() {
  AddEventListener("fire",new BurnListener);
}

function hGlowingOrb_AnyClick() {
  emit("fire");
}


So, because the pointer to the unextended (parent?) struct is passed, it points to the original extender function. So once fire is emitted, the original CustomListener.act() method is executed instead of the new BurnListener.act() being executed. Just exploring workarounds to create a neat JS like events API using AGS script. Not there yet, but it was fun to explore.

Crimson Wizard

Quote from: eri0o on Fri 20/09/2019 03:32:24
So, because the pointer to the unextended (parent?) struct is passed, it points to the original extender function. So once fire is emitted, the original CustomListener.act() method is executed instead of the new BurnListener.act() being executed.

Well, yes, since AGS script does not support virtual inheritance or type information, base struct pointers do not know what child struct they are pointing to and you cannot call functions from child struct using base pointer.

eri0o

OK, it took me only an year to understand this! The video below explains how to implement Virtual Functions in Assembly and it's very didactic. I had to watch FIVE TIMES to understand. (if youtube blow it up here's a backup)



So if I understood this, with an AGS struct, with simply jump to the function. To do what I wanted, we would probably need a different thing in AGS Script, a class, where that class implementation would have a virtual table, and support a new modifier so it could declare a virtual function, which would be then placed in it's virtual table. A class that extends the base class, when accessing using either pointer would go to the virtual table of the original object, thus doing what I intended in the code above.

Of course, AGS Assembly can't directly call a function if it's in other Script Module, I think in this case it needs to jump to the other script module offset and then call the function (with SCMD_CALL)? Anyway, this virtual table would have to be slightly different because of this to support extending a class from both the same script you are as a different script.

Even thought it's stuff that can't be done now, I think I vaguely see some light on how this could possibly be implemented.

Crimson Wizard

#13
If there's a function pointer type in AGS, the "virtual table" could be done in script in C- like way, simply as a user managed struct full of function pointers.
Then you set the pointer to such managed struct inside your type, and use it to call functions.

Alternatively, this may be a simple struct inside your type. That will lack automation and take more memory, as you have to fill the pointers for each new object yourself, otoh is also more flexible as you may also customize every object individually by setting different pointers, so it's more like a Lua approach perhaps.

eri0o

#14
note to any moderator reading this, feel free to split this topic!

Btw, in last video, one can skip to minute 26 where everything comes into play for the virtual function implementation.

Quote from: Crimson Wizard on Wed 07/10/2020 02:49:09
If there's a function pointer type in AGS, the "virtual table" could be done in script in C- like way, simply as a user managed struct full of function pointers.

Do you think a function pointer would be something like this?

Code: ags
function* (int)(int, int) foo;

int multiply(int x, int y)
{
  return x*y;
}


function applyFoo(function* (int)(int, int) func)
{
  int a = 5;
  int b = 6;
  return func(5, 6);
}

function room_Load()
{
  foo = multiply;
  int a = foo(3,5);
  int b = applyFoo(multiply);
}


With function pointers directly we need to figure out at run time if the function is in the current script module or in other, I thought with extending the classes this would be taken care in some other way.

Crimson Wizard

#15
Quote from: eri0o on Wed 07/10/2020 03:04:26
With function pointers directly we need to figure out at run time if the function is in the current script module or in other, I thought with extending the classes this would be taken care in some other way.

Using function pointers is an option, and it's a proper feature to have either way.

The function location is probably deduced at compile time in any case, as you need to do this when creating a value for assigning a function pointer (function adress).
EDIT: that, unless it's implemented in a hacky way where it's found at runtime. But proper function adress value is created at compile time.

EDIT: actually, because of how virtual table works, calling a function pointer and virtual function likely will work similarily in either case, the difference will be in whether the whole table is generated at compile time or at runtime by user.


EDIT 3: idk if there may be anything smarter than this, but in ags script, it seems, the "function pointer" can be simply a pair of module ID and bytecode offset (two ints?).

eri0o

Quote from: Crimson Wizard on Wed 07/10/2020 03:32:06
EDIT 3: idk if there may be anything smarter than this, but in ags script, it seems, the "function pointer" can be simply a pair of module ID and bytecode offset (two ints?).

Wait, you mean in a new instruction right? I could not find a instruction currently that does this.

Crimson Wizard

#17
Quote from: eri0o on Wed 07/10/2020 03:51:00
Wait, you mean in a new instruction right? I could not find a instruction currently that does this.

I mean, as an object in memory, it has to combine two values: module id and offset.

EDIT: in regards to instructions, SCMD_CALLAS currently does something similar, but "module id" argument is packed in a confusing way. And offset argument is passed as real memory address... Idk if this will work as-is for function pointers.

SMF spam blocked by CleanTalk