KeenSoftwareHouse / SpaceEngineers

2.94k stars 896 forks source link

Event system for scripts #29

Open kapitanov opened 9 years ago

kapitanov commented 9 years ago

First version of programming block had an unintentional but useful feature - terminal blocks had events (the very same events that you can find in ModAPI - for example IMyDoor.DoorStateChanged). Back in those days you could wire up an event handler from a programming block and execute a piece of code every time someone opens or closes the door, without any delays , workarounds or custom mod scripts. Of course this event handler would never get unsuscribed so it lived as long as the game session.

I propose to create some kind of event system for terminal blocks that we can use from programming block scripts. Below I give some code examples that illustrate the whole idea:

void Main()
{
   IMyTerminalBlock block = FindDoor("My Door 1");
   const string EventName = "Opened";

   // Subscribe to an event named "Opened" on block "My Door 1"
   AddEventHandler(block, EventName, EventHandler);
}

void EventHandler(IMyTerminalBlock block, string eventName)
{
    // Handle an event

    // Remove event subscription
    RemoveEventHandler(block, EventName, EventHandler); 
}

These AddEventHandler and RemoveEventHandler should perform invokation with respect to programming block state and should not prevent any garbage collection issues.

In my opiniom, this feature will allow players to create complex interactive scripts that work without any timer-based solutions and can process different kinds of events.

Let's discuss this idea, shall we?

Tyrsis commented 9 years ago

Events have to have processing limits, otherwise a server who has them enabled can be hung with bad scripts. I believe they were removed for this reason originally (there were probably more reasons than just that, but that's the reason I can think off hand)

MMaster commented 9 years ago

That can be theoretically overcome by limiting number of event callbacks to some reasonable amount per second(minute). Maybe passing list of events to callback instead of calling it for each event. That way the script would need to handle if there are too many events because otherwise it would just throw "too complex" exception. But yes, it is certainly not a simple task to prevent abuse.

kapitanov commented 9 years ago

It's a nice idea to limit number of event callbacks per second but what should we do when we reach the limit? It's not very good to just drop events since this might lead to unreliable scripting - most of the time scripts will react on events but sometimes they'll just ignore them. We can queue the events we were unable to handle but this event queue should be time- and size-limited since - events should be removed from the queue if queue has too many events or some events are too old to remain relevant. Again, this might lead to unreliable eventing.

MMaster commented 9 years ago

As I wrote in my post: Maybe passing list of events to callback. So every time the callback is called it will get list of all subscribed events (with timestamps) that happened since it was last called. Old events should not happen as the time to call callback needs to be reasonable - lets say 1 per second (not sure if its right value, just example because its the lowest delay on timer). That also kind of limits number of events that can queue up (depends on how many events can happen per second). And I think you can still safely say that when more than X (100?) events happen per second then you can start dropping old events because its probably abuse anyway. Scripters will need to take that into account just as they need to take into account number of instructions the script can do. But I'm not sure if that was the only reason why events were removed from ingame scripts.

rockyjvec commented 9 years ago

What about getting rid of callbacks and instead just having an event queue property on the PB. The game would put any events that occur into that queue and it could get cleared at the end of every PB run. Then, the script would be responsible for handling the events by iterating over the events in the queue. A callback system could easily be implemented on the in-game side if that was desired. Or, it could be implemented on the game side and you could just call something like pb.EventQueue.Handle() to trigger all the callbacks. That would eliminate any problems with things getting called too often since the script would still have to obey the "complexity" requirement.

You could even have the PB "subscribe" to events so the game would only need to put events into the queue that the PB was subscribed to.

MMaster commented 9 years ago

I think that is good approach too. Even tho that would not trigger callbacks exactly when the event happened even when no event happens for several minutes. And it would still require timer or similar mechanism. But it's probably more suited for scripting.

rockyjvec commented 9 years ago

True, although what I do is put the PB into basically a sleep mode where it only does basic logic each time it is called (checking for events, etc), then I set the timer block to trigger itself instead of using the delay timer/start. That effectively makes the PB run in real time. Maybe even close to once per game tick. It only needs to do more advanced/complex logic when an event is handled.

MMaster commented 9 years ago

I completely agree with you. That would be the most reliable way to get events in time when there are no callbacks. I don't use trigger now in timers because I just don't like the fact that something gets executed each tick when it doesn't need to :) I always do my scripts so they don't need to be executed that often, but there are certainly cases when there is no other option. I think you can also make the programmable block execute the timer trigger action when needed and let it run each second when not.

But I still see issue with this. Nobody can guarantee that PB that subscribed to some events will actually consume them (timer block not running or similar) so there needs to be some mechanism to throw away old events anyway.

rockyjvec commented 9 years ago

Yeah, maybe if an event isn't consumed after a given amount of time then it should be discarded.

Also, for me, the times I have needed to run the programming block very frequently was when I was trying to get user input for a menu or game. If the programming block could be triggered easily like other actions in the button panel, cockpit, etc. then I think most of that issue would be solved. Of course it would need parameters in that case. But a TerminalRunArgument property recently got added to the programmable block so maybe that will happen in the future: https://github.com/KeenSoftwareHouse/SpaceEngineers/search?utf8=%E2%9C%93&q=TerminalRunArgument

rockyjvec commented 9 years ago

Awesome!!! I was looking into that TerminalRunArgument some more and it turns out they added arguments to the programmable block!!! So when you attach the run action to a button panel, for example, it prompts what argument you want!!! Then the argument is passed into the Main() function. Yipeee! I've been wanting this for so long.

MMaster commented 9 years ago

Yep. I think that was Malwares work. Also: IMyProgrammableBlock Me; Action Echo; // show text in DetailsInfo TimeSpan ElapsedTime; // time since last call

malware-dev commented 9 years ago

Those features are in-game right now, if you didn't know :smile:

MMaster commented 9 years ago

Yes the ones that I wrote were your contributions. That's what I meant to say :) Or did you mean in-game programming events?

malware-dev commented 9 years ago

No I understood what you meant @MMaster, I did mean that the programmable argument is out, nothing else. It was a response to @rockyjvec :smile:

rockyjvec commented 9 years ago

Yeah, thanks so much for adding those, @lord-devious, I've implemented the new features in my scripts already and they make things SO much easier. Especially the PB arguments. :-) My menu example actually makes sense now since I don't have to use increase/decrease actions to get input, and I don't even need to use timers anymore in most cases. https://github.com/rockyjvec/EasyAPI/blob/master/modules/EasyMenu/Examples/Example2.cs

kapitanov commented 9 years ago

Programmable block already has a way to limit count of instructions in a script. Pehaps we can use the same thing to limit event handlers execution time? The actual limit can vary - MAX_NUM_EXECUTED_INSTRUCTIONS when running Main() function and MAX_NUM_EXECUTED_INSTRUCTIONS/10 when invoking a callback.

alex-davidson commented 9 years ago

An interrupt-based system for triggering scripts would certainly make the programming model clearer, for certain tasks. With a comprehensive list of subscribable events it would be extremely useful.

On the subject of instruction-counting, there's a trade-off which the script writer might want control over: events which need handling 'right now' and events which can be queued for handling when there's time. So 'real time' event handlers get preferred, and others get executed in ticks where there's no urgent events. That's probably overcomplicating things for the first iteration, but might be worth exploring in the future? It can be polyfilled with a bit of extra script code anyway, for the time being.