Client and Server Events
[edit] Client and Server Events
Originally Written by: Yoshiboy
Python Files Used: Module_Scripts.py, Module_Items.py and Module_Common.py
Part 1 - How to think about multiplayer modding
Ok, so the first problem we ran into with this sort of thing in Hunt was that gun sounds would only play on the computer of the player who shot them. Of course this wasn't really the end of the world - but it was a pain - so I looked into how we could get them to sync. In this tutorial I'm going to be going through how to add new server and client events, in this case to enable our gun sounds (or any other sounds) to sync between players. So the running example will be this gun event - but it should be applicable to basically all other events you'll need and problems of the same sort.
What I did first was take a look at all the new multiplayer commands. We've got some ones which look like this:
# multiplayer multiplayer_send_message_to_server = 388 # (multiplayer_send_int_to_server, <message_type>), multiplayer_send_int_to_server = 389 # (multiplayer_send_int_to_server, <message_type>, <value>), multiplayer_send_2_int_to_server = 390 # (multiplayer_send_2_int_to_server, <message_type>, <value>, <value>), multiplayer_send_3_int_to_server = 391 # (multiplayer_send_3_int_to_server, <message_type>, <value>, <value>, <value>), multiplayer_send_4_int_to_server = 392 # (multiplayer_send_4_int_to_server, <message_type>, <value>, <value>, <value>, <value>), multiplayer_send_string_to_server = 393 # (multiplayer_send_string_to_server, <message_type>, <string_id>), multiplayer_send_message_to_player = 394 # (multiplayer_send_message_to_player, <player_id>, <message_type>), multiplayer_send_int_to_player = 395 # (multiplayer_send_int_to_player, <player_id>, <message_type>, <value>), multiplayer_send_2_int_to_player = 396 # (multiplayer_send_2_int_to_player, <player_id>, <message_type>, <value>, <value>), multiplayer_send_3_int_to_player = 397 # (multiplayer_send_3_int_to_player, <player_id>, <message_type>, <value>, <value>, <value>), multiplayer_send_4_int_to_player = 398 # (multiplayer_send_4_int_to_player, <player_id>, <message_type>, <value>, <value>, <value>, <value>), multiplayer_send_string_to_player = 399 # (multiplayer_send_string_to_player, <player_id>, <message_type>, <string_id>),
All these seem to be about sending things between the players and the server. More on these later.
Also we have this one:
multiplayer_is_server = 417 # (multiplayer_is_server),
Which is a conditional thing in "try" blocks which only allows the server to execute the commands following it (you can also negate it with neg| to make it client only)
So basically the server and the client both run the whole module in it's entirety with the exception of the server running these "multiplayer_is_server" blocks and the client only running the negated ones. Most of the time this actually works really well for us - along with all the hard coded stuff this means almost everything is already synced. We only run into issues with a few things. Like when the player or server generated a random number - these will be different. Or when the player does certain things like executing the code after firing a weapon or the code after interacting with a scene prop - this is something the server doesn't track or take into account.
So when you're thinking about multiplayer modding, just remember the following:
- Each client and the server run the whole module in it's entirety with the exception of the server running "multiplayer_is_server" blocks and the client running the negated ones.
- The Server sees everyone else as just another "agent" or, in more depth, a "player". It doesn't keep track of what is happening at their end.
- Same goes for the client - everyone is just another "player" and all it tries to do is put them in the right position, doing the right thing.
Part 2 - Setting up our gun
So taking a quick look at the gun code:
["flintlock_pistol", "Flintlock Pistol", [("flintlock_pistol",0)], itp_type_pistol |itp_merchandise|itp_primary ,itcf_shoot_pistol|itcf_reload_pistol, 230 , weight(1.5)|difficulty(0)|spd_rtng(38) | shoot_speed(160) | thrust_damage(45 ,pierce)|max_ammo(1)|accuracy(65),imodbits_none, [(ti_on_weapon_attack, [(play_sound,"snd_pistol_shot"),(position_move_x, pos1,27),(position_move_y, pos1,36),(particle_system_burst, "psys_pistol_smoke", pos1, 15)])]],
It's fairly obvious what is wrong. The (play_sound,"snd_pistol_shot") just plays a noise on the client which uses it, and doesn't send data to everyone else.
(Note: the particle stuff seems to already have hardcoded events so we don't have to sync this.)
The solution is to use one of the new multiplayer commands, to send the data to the server, telling everyone that we've shot the gun, so that he can then relay this to all the other players. But all those commands take a <message_type> parameter. You might be wondering what this is. Well it's the event type - of which all the current ones are listed in header_common, and we need to set up our own before we can use the command. We're adding a new "client_event", because it is the client who is sending the message to the server. We'll call ours "multiplayer_event_sound_made_by_player"
... multiplayer_event_admin_set_friendly_fire_damage_self_ratio = 40 multiplayer_event_admin_set_friendly_fire_damage_friend_ratio = 41 multiplayer_event_admin_set_allow_player_banners = 42 multiplayer_event_admin_set_force_default_armor = 43 multiplayer_event_admin_set_anti_cheat = 44 # NEW EVENTS multiplayer_event_sound_made_by_player = 45 ...
(Note: In newer versions of M&B more events have been added, so the constant numbers above might have to be changed. Just make sure there are no two event numbers that overlap and that you don't go above 128 because this is the hardcoded limit. If you need more events, consider using sub-events by always using one event number but then passing an extra parameter to differentiate between what you want to do.)
Now changing the gun code so that it uses the event is fairly simple. We remove the "play_sound" command and replace it with our new message to the server. Idealy we'll want the server to relay this sound event back to all players - including the one who made the sound.
["flintlock_pistol", "Flintlock Pistol", [("flintlock_pistol",0)], itp_type_pistol |itp_merchandise|itp_primary ,itcf_shoot_pistol|itcf_reload_pistol, 230 , weight(1.5)|difficulty(0)|spd_rtng(38) | shoot_speed(160) | thrust_damage(45 ,pierce)|max_ammo(1)|accuracy(65),imodbits_none, [(ti_on_weapon_attack, [(multiplayer_send_int_to_server,multiplayer_event_sound_made_by_player,"snd_pistol_shot"),(position_move_x, pos1,27),(position_move_y, pos1,36),(particle_system_burst, "psys_pistol_smoke", pos1, 15)])]],
Part 3 - Telling the server what to do
So now we've got the gun sending a message to the server, but the server doesn't know what to do when it receives this message, so we need to add some code for that. Open up module_scripts and search for this script "game_receive_network_message".
This is a very important script. Basically it is the script which is called whenever any of the new "multiplayer_send_X_to_Y" commands are used, and the various script parameters correspond to the extra data which is send by the command. So in here, we want to add some code that is executed when it picks up on our messages. Slightly confusingly we want to add our code into the section labelled with "SERVER EVENTS" as it is the server which executes our stuff. We'll add our code right at the end so scroll down to just above where we have "CLIENT EVENTS" in a comment block and we'll add some code:
... (assign, "$g_multiplayer_force_default_armor", ":value"), (try_end), (else_try), # NEW EVENTS ADDED (eq,":event_type", multiplayer_event_sound_made_by_player), (neq,":player_no",0), (store_script_param, ":sound", 3), (get_max_players, ":num_players"), (try_for_range, ":cur_player", 0, ":num_players"), (player_is_active,":cur_player"), (multiplayer_send_2_int_to_player, ":cur_player", multiplayer_event_sound_at_player, ":sound",":player_no"), (try_end), (else_try), # END NEW EVENTS ADDED ############### #CLIENT EVENTS# ############### (neq, multiplayer_is_server), (try_begin), ...
Ok so we've got a few things going on here. First of all you'll notice a new event I've sneaked in - "multiplayer_event_sound_at_player". We'll need to add this to header_common, under the server events section (as it is the server who uses it)
... multiplayer_event_return_anti_cheat = 104 # NEW SERVER EVENTS multiplayer_event_sound_at_player = 105 ...
This is the event which the clients pick up, to tell them that some player (":player_no") has made a sound (":sound"). ":player_no" is a variable grabbed right at the beginning of the script, and corresponds to the player who send the event message to the server in the first place. ":sound" is just the sound variable which was sent by the player - we grab this from the script parameters.
Then finally we have a try for range block - which loops over all the players, and sends them this message. And we're done with the server.
Part 4 - Telling the clients what to do
Getting the clients to do the right thing is actually really quite similar. You might have spotted the massive comments block labelled "CLIENT EVENTS" and this is exactly where we are going to add our code for catching this new message the server just sent out. Scroll down right to the bottom of this "CLIENT EVENTS" block and we'll add our new code.
... (store_add, "$g_my_spawn_count", "$g_my_spawn_count", ":value"), (else_try), (assign, "$g_my_spawn_count", ":value"), (try_end), # NEW CLIENT EVENTS (else_try), (eq, ":event_type", multiplayer_event_sound_at_player), (store_script_param, ":sound", 3), (store_script_param, ":player", 4), (player_get_agent_id,":agent",":player"), (try_begin), (neq,":agent",-1), (display_message,"@BANG!"), # This is just a debug message, it isn't essential. (agent_play_sound,":agent",":sound"), (end_try), # END NEW CLIENT EVENTS (try_end), ]), ...
So this is all fairly straight foward. We test to see if it is our new event, if so we grab the parameter for which sound was made and which player made the sound. Then we find the agent that player uses and get it to make the sound. I've also added a debug message for testing. To stop some errors we also have to test for a null agent.
Part 5 - You're done
And this is it. Believe it or not you're done. Compile your code, load up the server running your new mod, get a couple of players on there and shoot away.
Finally it is probably worth noting the difference between dedicated servers and hosted games and how this effects your code. The main difference is that in hosted games, player number 0 is also a player playing the game (and as such will have to be sent sound events and the like) where as with a dedicated server, player number 0 is always the server. This can get you into different bits of trouble if you're not careful and I believe there have been introduced some other peculiarities in recent versions too which I'm not familiar with. The way to see if the game is running as dedicated or hosted is to use the command "multiplayer_is_dedicated_server".
Lots of people have been saying they did this tutorial exactly and that it didn't work (i.e didn't play any sound). I'd love to help you, but realize that I cannot read minds. Unless you give me some more info then I don't have any more idea than you on how to get it working. What you should do at this point is try adding in some debug messages using "display_message" to see what parts of the code are getting executed and which are not. Really any info will help me tell you where things might be going wrong.
If anyone finds any errors in the tutorial or has any issues then please say. I did test it in-game and it was being a bit funny (although the code is exactly the same as for hunt so I'm not sure why).