[CLOSED] Hybrid GM (Dev-Log)

Started by ., Feb 27, 2015, 07:07 PM

Previous topic - Next topic

.

I am reserving this topic for a plugin that I'm working on. It's an alternate version of the Squirrel plugin designed for advanced scripts and script-writers. The source isn't available at the moment because it's not complete. I am creating this topic to make it like a personal journal in which I discuss the various concepts that I'm working on for this plugin. Also to keep me motivated and probably collect some thoughts from you (the script-writers) in the process of creating it.

Right now this plugin is just a bunch of scattered files used in my tests. I will start posting here the various concepts I plan to implement in this plugin. If the staff doesn't allow this kind of threads than please lock it. Please do not ask for a release date because it's just a work in progress (like the title says). Thank your for your time.

Also, please do not post something unnecessary here like "Thanks!", "I'll be waiting for this!", "You suck!" etc. Only your thoughts on the concepts that I discuss or some ideas you think will be useful.
.

Thijn

Nice idea. Irrelevant posts will be removed to keep this topic as clean as possible.

.

#2
"START LISTENING"

Sweet, sweet, events. The beating heart of every system out there. They're the ones that tell you when to react and how you should react. To differentiate this plugin from the rest of them I had to do so major work here. So let's go slowly through the new event system in this plugin by analyzing the one in the current Squirrel plugin.



You all know the current event system and how it works. The Squirrel plugin has some various fixed function names that it expects in the global namespace of your script. Names like 'onServerStart', 'onServerStop', 'onPlayerJoin', 'onPlayerPart' etc.

function onServerStart()
{
print("We're live!");
}
function onServerStop()
{
print("We're off!");
}

function onPlayerJoin(i_player)
{
print("Welcome to our city!");
}

function onPlayerPart(i_player, reason)
{
print("Have a nice day!");
}

On each event the Squirrel plugin looks into the global table for an element with that name. If an element is found and that element is a function then the plugin executes that function and forwards the arguments received from the server. It's a nice and simple implementation but sadly not my style. It can get quite tedious when you start to rule out various calls based on the passed arguments. So let's move to the new event system and see the differences.



The new event system ditches global functions in favor of OOP (OOP is used internally, you don't deal with it). Since Squirrel functions can be treated like lambdas. That means you can capture a function in a variable if you want:

local a = function(a, b, c) { return a + b - c; }
 print( a(8, 3, 2) ); // Should output 9

Although lambdas in Squirrel look different and they're prefixed with '@' so they would look like ''local a = @(a, b, c) return a + b - c;". But both expressions work and that's not the point of this discussion.

The point was to see how a simple event would be declared in the event system I'm working on. So let's look at a simple one:
local my_event_listener = server.event(EVENTID.PLAYER_DISCONNECT);

my_event_listener.on_trigger = function(player, reason)
{
print("I got bored!");
}

First we create an instance of the event listener. The event listener automatically starts listening to the event associated with the ID that you specified. In this case the player disconnect event. But that doesn't do anything because when the event is triggered there's no one to listen or react. You can also pass a bool after the ID to specify if you want the event to be suspended or not.  What I mean by that is if you pass 'true' then the event will not trigger until you unsuspend it later on.

That event instance has a couple of member variables which will discuss in later posts. Because the event system is quite complex and has many features, each feature will be discussed in a separate post. Right now we're interested in the 'on_trigger' member variable. This variable contains the function you wish to get called when the event is triggered.

When the event is triggered it looks to see if there's a function inside the 'on_trigger' member variable. And if there is, then that function is executed and the arguments from the server are forwarded automatically. I also don't have to search the global table every time an event is triggered. Which may save some performance for large game-mods with a crowded global table.

If you look at this right now you might think it's a waste of time. But in later posts I will make you think otherwise. This is just an introduction to give you an idea of it looks. I'll see you in the next post ;)
.

.

#3
YOU SHOULD SLEEP FOR A WHILE

In this post we explore the 'idle' member function which expects an integer. This is the ability of the event system to stay idle for a specified period of time. That means the trigger function you specified won't get triggered until that time has passed. Even if the event is triggered by the server internally, it's simply ignored. The idle time is specified in milliseconds. And can be specified wherever you want in the code. Even in the function you specified to be triggered.

Let's see how we can make a small timer using this functionality:

local my_event_listener = server.event(EVENTID.FRAME);

my_event_listener.idle = 5000; // Initial: Sleep for 5 seconds

my_event_listener.on_trigger = function(delta)
{
print("5 seconds have passed!");

my_event_listener.idle = 5000; // Sleep for another 5 seconds
}

We start by listening to the frame event because this is called very frequently. Then we tell it to sleep for 5000 milliseconds which means 5 seconds. We could avoid the initial sleep and allow the function to be called right away. But I just added that as an example.

Then in the triggered function that we specified we tell the event to sleep for another 5000 milliseconds each time the function is called. This isn't a very effective timer but it's an example of what you can do with this functionality. The final plugin timer will use a similar approach but in a much more optimized way.

See you in the next post with the next feature :)
.

.

#4
CAN YOU PLEASE STOP SPAMMING ME!

In this post we explore the 'stride' member function which also expects an integer. This is the ability of the event system to skip a certain amount of event calls before triggering the specified function. Maybe you don't want your function to get executed every time the event is triggered.

Say you just want to give 1000$ to the 5'th player that connects to your server. Wouldn't it be cool if the event system would do that for you? Instead of you keeping track of this in some god forsaken variable somewhere in your script.

Let's see how we would use the 'stride' member variable to give $1000 to every 5'th player that connects to your server:
local my_event_listener = server.event(EVENTID.PLAYER_CONNECT);

my_event_listener.stride = 5;

my_event_listener.on_trigger = function(player)
{
player.money = 1000; // You lucky bastard ;)
}

We start as usual by attaching to the event we want. In this case the player connect event. Then we set the stride to 5. This means that our trigger function won't get called until this event was triggered 5 times in the server. After the event was triggered 5 times (internaly), our function gets executed and we receive the arguments associated with this event type as usual. Then this process starts over and the event is skipped for another 5 times.

Associated with this functionality is the 'ignore' member variable. Which can be used anywhere in your code to query how many times there's left to skip that event. So let's go step by step in this. Let's say that 3 players have connected since that event was created. You don't know that but somewhere in your script you realize that you need to know that. Well, you can always ask that from your event instance using 'my_event_listener.ignore' which, if we follow this fictional story should return 2. Because our stride is 5 and 3 players have connected so far....

You can also change the remaining times to skip using the 'ignore' member variable. Because that's a getter/setter you can simply adjust that anywhere in your code.

Let's say that every time a player disconnects you want to increase the remaining amount of events to ignore. Let's see how we would do that:

local my_event_listener = server.event(EVENTID.PLAYER_CONNECT);

my_event_listener.stride = 5;

my_event_listener.on_trigger = function(player)
{
player.money = 1000; // You lucky bastard ;)
}


local another_event_listener = server.event(EVENTID.PLAYER_DISCONNECT);

another_event_listener.on_trigger = function(player, reason)
{
my_event_listener.ignore += 1;
}

Now, each time a player disconnects, another player must connect to actually receive the $1000 prize. The stride still remains 5. You just altered the current times the event has to skip in the current iteration. Hell, you can even make it bigger then the stride if you want.  Except when all those events are skipped the value goes back to 5 like the stride. You can make it 100 and then the event will be skipped 100 times before being reverted back to the initialized stride value. Which in this case was 5.

In the next post we use what we have so far to get an idea of how the new event system works so far. Then we proceed to the remaining features :)
.

.

#5
WHAT CAN WE DO WITH SO LITTLE

Using only those two features we'll create a small script that prints every 60 seconds how many players have connected/visited since the server was started. This isn't the way to do this but let's see how fun is to combine these :)

Let's have the first half of the code first and then proceed to explaining it:
local my_event_listener = server.event(EVENTID.PLAYER_CONNECT);

// This will be defined as the maximum value an integer can hold
my_event_listener.stride = SQMOD.INT_MAX;

my_event_listener.on_trigger = function(player)
{
// Dummy event and we don't care about it
}


We start by creating an event listener instance which is triggered each time a player connects to the server. And then we set a stride to some ridiculous number. In this case we use the compile time value defined in "SQMOD.INT_MAX". That define will be available in the plugin and is not a part of Squirrel API. On 32 bit platforms that value can be in the ranges of 2,147,483,647. You can see that is a ridiculous value to have as a stride.

We don't need this event to actually execute any function. In fact we don't even have to assign a trigger function. We just need it to count how many times it was triggered internally in the server. In this case how many players have connected. So that we can use the 'ignore' member variable later on to extract the actual amount of visitors.

Now let's have the other half of code and proceed to explaining it as well:
local another_event_listener = server.event(EVENTID.FRAME);

another_event_listener.sleep = 60000; // Sleep for 60 seconds initially

another_event_listener.on_trigger = function(delta)
{
announce("Visitors on our server: " + (my_event_listener.stride - my_event_listener.ignore);

another_event_listener.sleep = 60000; // Sleep for another 60 seconds
}

In this half of code we create another event listener instance which listens to the frame event. You can see that we're using the same trick we did earlier to get a temporary timer working. We make this event sleep for 60 seconds initially and then another 60 seconds after each call. Yay, we have a 60 seconds timer.

In the function we specified to be triggered we make use of the 'ignore' member variable from our previous event listener. The one with the stride. And we simply get how many times there's left to ignore that event. Then we subtract that from the stride we set and we get how many times that event was triggered.

Now that we know how many times have connected since the server started we make an announcement and make the event sleep for another 60 seconds. How easy was that?

I'll see you in the next post with the next feature :)
.

aXXo

Some existing events are called way too frequently (example: onPlayerMove) and hence are largely unused due to performance issues.
Will the 'stride'/'sleep' functionality help that cause?

.

#7
Quote from: aXXo on Feb 27, 2015, 11:02 PMWill the 'stride'/'sleep' functionality help that cause?

Definitely. But be patient and wait for the rest of the features that I'm documenting as we speak.  I'm working on the subsets now but the next feature which will be entity filtering and that will definitely help with this issue as well.
.

.

#8
"JUST A FINGER NOT THE WHOLE HAND!"

In this post we explore the benefits of subsets and what they are. There are only two subsets available and you have to use them wisely to not exhaust them. Those can be controlled by the 'primary' and 'secondary' member variables which also work with integers. The subsets allow you to filter the server events to listen only for what you need. Basically it allows you to take the arguments of a server event that is of type integer and see whether you want your function to be executed or not.

Probably you are a bit confused right now. So let's take this slowly by walking you through how the events work on the server. There are events in the server that are actually a group of events. Like a set of events, hence the name sub-set. Take the 'onPlayerActionChange' event. That single server callback is a group/set for 12 events (actually 13 if you count the null event) which are:
0 - none
1 - normal
12 - aiming
16 - shooting
41 - jumping
42 - lying on ground
43 - getting up
44 - jumping from vehicle
50 - driving
54 - dying
55 - wasted
58 - entering vehicle
60 - exiting vehicle

Let's see how this looks without subsets first by trying to figure out when a player is jumping from a vehicle:
local my_event_listener = server.event(EVENTID.PLAYER_ACTION_CHANGE);

my_event_listener.on_trigger = function(player, previous_action, current_action)
{
if (current_action == 44) {
print("Suicidal maniac!");
}
}

We start as usual by creating an event instance which listens to the player action change event from the server and then assign a function that we want to get triggered when that happens. Then in our assigned function we test if the current player action is the one that we need and print a simple message. Not bad, right? But I think we can improve that.

These events are are passed as an integer argument to your specified function. And then you have to do a bunch of unnecessary tests on those arguments if you only need one of them. Wouldn't be nice if the event system would do that for you? Would definitely save a lot of time and make things much simpler.

This is what subsets were designed for. They allow you to extract the parts you need from a server callback and completely ignore the rest of the calls. This takes some load of the script (and script-writer) and make things much more clear.



Remember when I told you that these subsets can be exhausted? Well, if you look at the event in the example above you can see that the event has two integer arguments. And how many subsets do we have? Exactly 2. Which means that only one subset per argument can be set.

In this case the primary subset will filter the previous player action and the secondary subset will filter the current player action for this server event. But what about a server event that only has one integer argument. Like the player disconnect for example which has the 'reason' argument. Does that mean we have one subset remaining which does nothing?

Not actually. In those callbacks both subsets are used for that argument. Which means that you can test for two things at a time. That's what I meant earlier when I said that there are only two subsets and it's important that I use them properly.



So, how do we make the above example to use these subsets that we talked about. Well, let's see:
local my_event_listener = server.event(EVENTID.PLAYER_ACTION_CHANGE);

// 44 is the id of the "jump from vehicle" action
my_event_listener.secondary = 44;

my_event_listener.on_trigger = function(player, previous_action, current_action)
{
print("Suicidal maniac!");
}

Again, we start as usual by creating our event. Except this time we make use of the 'secondary' member variable which controls the secondary subset. And if you remember correctly when I told you that because this server event has two integer arguments the secondary subset would be occupied by the second integer argument. And that argument is none other than the current player action.

Now, each time that event is triggered in the server the event instance that we created checks to see if the value in that argument matches the one in our subset. And if the values match then the event instance triggers the function that we assigned to it.



Now you might say: But you haven't used the primary subset. Well, in this case it wouldn't be that necessary but let's do that for the same server event. Let's make this event even more strict and tell it to only execute our function when the current action is 44 (jumping from vehicle) and the previous action is 50 (driving). Doesn't make too much sense but let's try that.

local my_event_listener = server.event(EVENTID.PLAYER_ACTION_CHANGE);

// 50 is the id of the "driving a vehicle" action
my_event_listener.primary = 50;

// 44 is the id of the "jump from vehicle" action
my_event_listener.secondary = 44;

my_event_listener.on_trigger = function(player, previous_action, current_action)
{
print("Suicidal maniac!");
}

This time our assigned function will only get called if the id of the previous action is 50 and the id of the current action is 44. That means it get's called only if the >driver< jumps from vehicle.



But how does an event with only one integer argument look like? (you might ask) Well, let's do that with the player disconnect event which has an integer augment.

local my_event_listener = server.event(EVENTID.PLAYER_DISCONNECT);

my_event_listener.primary = EPARTREASON.TIMEOUT;
my_event_listener.secondary = EPARTREASON.CRASHED;

my_event_listener.on_trigger = function(player, reason)
{
print("Something went wrong with this client!");
}

We start as usual by listening to the player disconnect event. Only this time we activate both subsets to make our function only get called when the player disconnected abruptly. Since there's only one integer argument then both subsets can be used on that single argument. Which means that the specified function only get's executed if the event argument matches either one of the subsets values we specified.

Also note that we used two built in compile-time values that will be available in the plugin (they're actually enumerations). These are 'EPARTREASON.TIMEOUT' which will be equal to the timeout reason ID (aka. 0) and 'EPARTREASON.CRASHED' which will be equal to the crashed reason ID (aka. 3). This is to allow the script to still function if those values are ever changed in the server. You should avoid hard-coded values whenever possible.



Subsets can be changed anytime and wherever you want in your code. This gives you more control over your script at run-time. They can also be deactivated by setting them to -1. But if you remember correctly I told you to avoid using hard-coded values. That value will probably be available through compile time define like 'SQMOD.DISABLE_SUBSET'. You probably also realized that subsets only work with positive values.

Now that we discussed about the subsets it's time to move to the next feature. So I'll see you in the next post :)
.

.

#9
"TOO MANY VOICES IN MY HEAD!"

Time to discuss about entity filtering and how can we get events only from the entities we want and ignore the rest. This subject is pretty much self explanatory. The feature is still just on paper but it's too straight forward to not work. The only thing thing that might differ in the final version might be the member function names depending on how successful I get with the function overloading in Squirrel.

I haven't tested this yet but I think the function overloading in Sqrat is done by the amount of arguments instead of the argument types. Which means that some functions might have the entity names prefixed to them like 'filter_player()' instead of just 'filter()' where the argument type decides what to filter. Anyway, for the purpose of this post I'll assume that the function overloading will work based on data type and use non-prefixed names.



As usual we start with a common situation that demonstrates how this works. Time to put our imagination to work again. Let's say that you want to intercept every chat message of the spectating players. Let's start with the first half by creating an event that does that:
local my_event_listener = server.event(EVENTID.PLAYER_CHAT, true);

my_event_listener.on_trigger = function(player, message)
{
print("Spectator just said" + message);
}

We create the event listener instance to get the event we want and we assign the function to be triggered as usual. Except there's something different when we create the event instance this time. Did you noticed that 'true' argument after the event ID? If you remember correctly I told you that we can control whether the event instance should forward the server events immediately or be suspended until we enable it back.

If we wouldn't suspend this event instance then we would receive the chat messages from all the players (spectators or not). Because there's no player in it's entity filter at the moment. So, this time we make use of the 'suspend' member variable to control when this event should execute or not.

That event alone doesn't do anything so it's time to see the second half which controls that:
local another_event_listener = server.event(EVENTID.PLAYER_SPECTATE);

another_event_listener.on_trigger = function(player, target)
{
my_event_listener.grab(player);

if (my_event_listener.suspend) {
my_event_listener.suspend = false;
}
}

local yet_another_event_listener = server.event(EVENTID.PLAYER_SPAWN);

yet_another_event_listener.on_trigger = function(player)
{
my_event_listener.drop(player);

if (my_event_listener.player_count == 0) {
my_event_listener.suspend = true;
}
}

Like we did earlier we create the event we need and assign our function. Except this time there's two events that help us identify spectating players. So let's go through that code and see what it does. But first let's explain what those new member functions do since we haven't mentioned them before.

Like we discussed previously there might be a chance that you'll have these functions prefixed with an entity name but we'll ignore that for now. The 'grab()' member function takes the entity instance that you specify and adds it into the internal entity filter. The 'drop()' member function does the opposite by taking the entity instance that you specify and removing it from the internal entity filter.

So using these two functions we control which players we should trigger the event in the first half of code or not. In our first event we listen for the player spectate event. And for every player that triggers that event we add it to the filter of our first event instance.

But right now that event might be suspended (actually it is suspended, initially). So we check to see if it is suspended and unsuspend it using the 'suspend' member variable. When you make the 'suspend' member variable to 'true' then the event is suspended and every event call is ignored during that time. When that variable is set to 'false' then the event is no longer suspended and your function is executed again.

Using the second event that we created and which listens to the player spawn event. We control which players are no longer spectators. And we use the 'drop()' member function to remove them from the internal filter of the first event that we created.

In the last event function we also check to see there's any player entity in the filter. And if there's no entity in that filter then we get executed for each player chat message like in the beginning. So we need to suspend the event back when that happens.

In the final implementation you might not need to worry about suspending the event. Because there might be two event types Local and Global. This is because I have to reduce the amount code which manages the event distribution internally in the plugin. Which means that this feature is subject to change and might different in the final implementation.



There's a lot more member functions/variables involved with filters and they'll be discussed when the plugin is actually released. One of them being the 'inverse_*' member variables (prefixed for each entity type) which can reverse the effects of a filter. When you set them to 'true' the filter is inversed and when you set them to 'false' the filter works normally.

That means instead of receiving events from the entities in the filter you end up receiving events from entities which are not in the filter. This could get handy sometimes but it's also subject to change depending on how much code is required to manage the event distribution.

There's more to entity filtering but this is the basic approach that I'm taking at the moment. That's about it for tonight. I'll see you in the next post discussing other features I'm working on :)
.



.

#12
Turns out I was right about method overloading in Squirrel. Actually I was expecting that :D Therefore the methods will be prefixed with the entity type name in the final version of this plugin. Also instead of having those grab_*()/drop_*() member methods which don't really express the intentions. I decided to rename them to include_*()/exclude_*() which means include this into the internal filter and exclude this from the internal filter.

Also after further investigations there will be 2 separate event types. Those are Global and Local. The global event will bind (internally) to the global event pools and listen for each call of the specified event. The local event on the other hand will only bind (internally) to the local event pools of each entity. Which reduces the overhead of calling functions who don't really need to be called.

Last time I also said there will be an option to inverse the internal filters. The previously presented implementation was also abandoned. The functionality still exists except in a different form. By default, the global event listener is has it's internal filter inversed. Which means that whatever is included in those filters will automatically stop at the C++ level and never reach the script level to trigger the specified function.

The local event listener has a normal filter by default. Not because the global event filter is inversed by default but only because it's the way is supposed to be. If you remember correctly I said that the local filter will only bind to the local event pools for each specified entity. And therefore its only normal for this event type to only listen to the entities in the internal filters.

There will also be a few other binding points that you can use in the event instances and not just the usual 'on_trigger'. Next to that there will also be:

  • on_included - Called each time an entity is added to the internal filters
  • on_excluded - Called each time an entity is removed from the internal filters
  • on_cleared - Called each time an internal entity filter is cleared/emptied/reset.
  • on_destroyed - Called each time and entity is destroyed elsewhere and must be removed from the filters.

The above binding points will also reduce the amount of code required to react/addapt to filter changes in other places where entities are added or simply removed from the filters.

There might be a few other changes to the entity filtering system but for now that's the general idea. The current changes in the event system are meant to reduce the amount of C++ code required to manage the event distribution internally and to have a clean implementation.  I won't hurry the plugin release because I want to have a strong implementation that will last for a long time :)
.

.

#13
Project was postponed after a rage quit this night. The implementation was close to completion but that f*ing compiler failed to do as told and as soon as I started to add more code to the implementation I'd get:
Plugin error >> LoadLibrary() 'plugins/sqmod.dll' failed: Code 998Failed to load plugin: sqmod
That error code is a b!tch to debug. It's triggered by the smallest things. For example the first time I got that message it was because of a `Sqrat::RootTable` as a member inside a class. And as soon as I changed that to a pointer it began to work again. Until the next thing happened and it started again and again until I just said "f* you!" and quit.

So after I took the pros and cons of the project I realized it's just not worth the trouble.
  • Too much crap to deal with.
  • Some limitations in the server just make my life a nightmare.
  • I could use the time for something better.
Anyway, I might continue to work on it another time, perhaps when the server improves a little and I find that it's worth the time and trouble. I'll refrain from writing more right now because I'm quite pissed of and bad things come out when I do that.
.

.

#14
After taking some time off and listening to some music to take the steam off and relax a little. I decided to give one more try and see why that god damn error pops out of nowhere. Previous searches on google gave me sh!t, literally sh!t. Just some whiny people on some forums with similar problems for 3DS Max, DirectX, Cinema something.. etc. And the common answer was, "Upgrade your software." and then miraculously "The bug was fixed.". And I was like. Well... how the f* does that help me? Obviously the "upgrade your software" wouldn't apply to me.

So I decided to go deeper with the searching and luckily I found a few crumbs of information somewhere on some forsaken topics. Which suggested that I should pay attention to some of my static variables. And so I did. Ending up starting the project again, from scratch and trying to eliminate static variables as much as possible. Turns out this was it. The nightmare that kept me so frustrated these days was finally gone. I still don't know the exact cause and which static variable was the culprit. I'm just relieved that it's gone.

So I'm happy to announce that the project development was resumed. With a slower pace now. Mainly because I lost my interest after wasting a lot of time and on that error. At least now I know that I can safely write code that I don't have to throw away later on. That error must have been the cause of almost 5 re-writes of the plugin. I just couldn't understand what was going on.
.