Jump to content

[TUTORIAL] How to use Public Variables


Recommended Posts

FYI, this guide is for advanced arma coders.  This guide will assume you know how to pack/unpack PBO's, edit files, and are comrfortable with coding.  You must also have an understanding of locality as in client vs server side.

 

A note on locality:  Your mpMissions folder is NOT only client side.  When I started coding I made this assumption and I was wrong.

 

 

Lesson 1:  Event Handlers Basics

 

Event handlers are the bread and butter of Arma2 MP coding.  They are little logic functions that the server sets aside until they are called by a public variable.  In my experience they have very low overhead so don't be afraid to make use of them.

 

example 1: Change players skin

Add this code to the bottom of your init.sqf (in your mpMission)

// expects: [_playerUID,_CharacterID,_skinClassName]
// this line says ONLY run on the ClientSide.  The server will see this and ignore it.  This means that when a client gets this command, they and only they will execute this.
if (!isDedicated) then {
  "PV_ChangePlayerSkin" addPublicVariableEventHandler {
    // handle our packet.  _this select 0 returns the PV name, _this select 1 returns our input packet
   _packet = _this select 1;  // should contain [_playerUID, _characterID, _skinClassName]
   _playerUID = _packet select 0;
   _characterID = _packet select 1;
   _skinClassName = _packet select 2; 
 
   // set our player to the skin
   [_playeruid,_characterID, _skinClassName] spawn player_humanityMorph;
  };
};

Ok so now we got our event handler set up, if any client sends the PV "PV_ChangePlayerSkin" all clients connected will execute this event handler.  Here is how we send the command to all players.  This code can be called anywhere, on a client, or from the server itself.

 

example:

  PV_ChangePlayerSkin = [_playerUID,_CharacterID,_skinClassName];
  publicVariable "PV_ChangePlayerSkin";
But you may be asking, how do I set just a single player's skin instead of everyone on the server?
 
We do this with publicVariableClient.  I am unsure if you can call this command from clientside as I only use it serverside but here is how you do it regardless.
// lets assume we want to set our cursorTarget's skin (the player we are looking at)
_player = cursorTarget;
_owner = owner _player; // owner command returns the client # we will need for the next step.
_playerUID = getPlayerUID _player;
_characterID = _player getVariable ["CharacterID","0"];
_skinClassName = "FR_GL"; // my personal skin


PV_ChangePlayerSkin = [_playerUID, _characterID, _skinClassName];


_owner publicVariableClient "PV_ChangePlayerSkin"; // only send the PV to the specific client

 

Now when we send our public variable we only send it to the client we wish to.  There are a myriad of ways to get player objects loaded into memory but that will be covered later.

Link to comment
Share on other sites

Lesson 2: Handling JIP clients

 

What is JIP? JIP stands for "Join in Progress" aka players who join after the mission has already started.  

 

You know how to change a players skin by sending out public variables but how do you make sure every player who joins has their skin changed?

 

For this example, we will assume there is an event running and for some reason you want to make sure every player who joins the server knows the event is running and if the event is started, lets make sure they get their skin.

 

We will also be introducing the idea of server side and client side event handlers in this lesson.

 

Brief flow of events:

* Client connects

* Client asks server if event is running

* Server checks to see if event is running

* If event is running, server changes players skin

 

Server Side JIP Check and Validator:

This code will handle requests from the client.


// server side check, make sure only the server runs this code
if (isDedicated) then {

	// DEBUG: Manually set our event to running...  You would normally do this thru a menu etc.
	// note, that this variable is only set on the server itself, so the client has no idea of the value
	// if you wanted, you could pass this thru instead of a skin but that's out of the scope of this tutorial
	Server_Event = true;

	// expects: [player]
	// Handles our client JIP requests
	"PV_JIP_Event_Check" addPublicVariableEventHandler {
		private ["_packet","_player","_owner","_skinClassName"];
		// deserialize our packet
		_packet = _this select 1;
		_player = _packet select 0;
		_owner = owner player;
		
		_skinClassName = "FR_GL"; // my personal skin
		
		// check to see if Server_Event has been set by an admin/script.
		if (!isNil "Server_Event") then {
			// validate Server_Event is true, if true, our even is running
			if (Server_Event) then {
				// our event is running, change skin of JIP player
				PV_ChangePlayerSkin = ["FR_GL"];
				_owner publicVariableClient "PV_ChangePlayerSkin";	
			};
		};
	};
};

Client Side JIP Check:

This code will be fired off when the client finishes loading their mission.

// client side, JIP checker, this runs whenever the user loads their mission file
if (!isDedicated) then {
	// call our JIP check to see if a mission is running
	PV_JIP_Event_Check = [player];
	// only send the public variable to the server!
	publicVariableServer "PV_JIP_Event_Check";
	
};

Client Side Skin Changer:

A slightly modified version of the 1st lesson's skin changer PV handler.

// client side, Skin changer, this runs whenever our server sends the PV to the client. Modified to use less bandwidth

// this line says ONLY run on the ClientSide.  The server will see this and ignore it.  This means that when a client gets this command, they and only they will execute this.
if (!isDedicated) then {
  // expects: [_skinClassName]
  "PV_ChangePlayerSkin" addPublicVariableEventHandler {
	private ["_packet", "_skinClassName", "_playerUID", "_characterID"];
    // handle our packet.  _this select 0 returns the PV name, _this select 1 returns our input packet
   _packet = _this select 1;  // should contain [_skinClassName]
   _skinClassName = _packet select 0; 
   
   // since we are only running this on the local client, we don't need the playeruid and characteruid passed
   // we can just grab them from the local player object
   _playerUID = getPlayerUID player;
   _characterID = player getVariable ["CharacterID","0"];
 
   // set our player to the skin
   [_playeruid,_characterID, _skinClassName] spawn player_humanityMorph;
  };
};

Place all of this code (in this order if you like) at the bottom of your init.sqf in the your mpmission.

 

Now when a player connects to your server, they will have their skin changed.   If you want to play around with this, You could add a custom menu in "fn_playerActions.sqf" that only admins see that sends a PV to the server to say set "Event_Status" to true of false.  However, this guide is quick and dirty so it will always set a player skin, since we declare "Event_Status" as true.

Link to comment
Share on other sites

Lesson 3: PV security

 

There is an old saying, that you can't have total security and total freedom at the same time.  BI seems to favor freedom over security which can be a good thing but we all know the darker side of this as server admins / scripters.

 

So how do we apply security to a game with almost no concept of it?

 

1. Localization.  Not quite security but it allows us to keep all of our most sensitive code (accessing the database with your custom addon for example) away from prying eyes.  This also lets us store values away from clients that they probably should not see.

2. Logging. Lets us see who called what and when.  Possibly even where if you want to go that far.

3. Analysis.  Using a tool like battleye to analyze our public varibles and values to see if bad code is being passed through.  This also covers manual and automated log scanning.

4. Validation.  Confirming our code is run by who we want it to and preventing execution from outside our environment.

 

It should be clear that there is no way to be 100% secure in your arma2 code especially with all of the hackers around.  An anti hack solution such as infistar is recommended however, I can't garuntee there will not be conflicts.  You will have to resolve these yourself (or with infistar's help).

 

hambeast note: PV's when sent out to everyone are only executed on machines that are not the sender.  So if you want your own client to get event to fire, you must pass it to the server first, then the server will send it out to everyone (except the server of course)

 

In this guide, I will attempt to cover all of the key points above and show you how to implement or integrate them into your system.  This lesson will show you how to create an admin menu which allows us to start and stop events.  When the event is started, all players (including JIP) should have a menu shown to TP to a predetermined location.  When the event is finished, the menu will disappear.

 

Here's a brief flow of the events:

 

1. Admin uses menu to start event

2. Players get message stating event has started

3. Players get menu to tp to event if they like

4. When admin ends event, players menu goes away

 

Server Side:

Open up your dayz_server pbo and edit init\server_functions.sqf.  This will be where we keep our server side event handlers for now.

 

Add the following somewhere near the top.  This is our list of admins.  You may have another method of populating this but this will work.

AdminList = ["123456789","12121212"];

Now add the following near the bottom somewhere.  These are our event handlers

// expects: [sender,status]
// set the status of our event and send to all connected clients
if (isDedicated) then {
	"PV_EventSetStatus" addPublicVariableEventHandler {
		private ["_packet","_sender","_status","_logString","_isAdmin",];
		_packet = _this select 1;
		_sender = _packet select 0;
		_status = _packet select 1;
		
		// check to see if user is an admin too.
		_isAdmin = (getPlayerUID _sender in AdminList);
		
		// validation: make sure our user in the admin list
		if (!isNil "_status" && _isAdmin) then {
			// log who sent it, we can cross reference against BE logs to see if people
			// are falsifying PVs
			_logString = format ["sender: %1 status: %2", _sender, _status];
			diag_log _logString;
			
			// set status serverside.  this value will stay until the server restarts
			Dayz_Epoch_Event_Running = _status;
			
			// send the value down to all clients
			PV_EventStatusClient = [Dayz_Epoch_Event_Running];
			publicVariable "PV_EventStatusClient ";
		};
		
		
	};
};

// expects: [sender]
// handle event checking status
if (isDedicated) then {
	"PV_EventStatusCheckJIP" addPublicVariableEventHandler {
		private ["_packet","_sender","_owner"];
		_packet = _this select 1;
		_sender = _packet select 0;
		_owner = owner _sender;
	
		// make sure our variable has been set, if not ignore it
		if (!isNil "Dayz_Epoch_Event_Running") then {			
			PV_EventStatusClient = [Dayz_Epoch_Event_Running];
			_owner publicVariableClient "Dayz_Epoch_Event_Running";
		};
	};
};

Client Side:

Open up your init.sqf in your mission and add the following to the top.  This is our client side admin list.  Not as important as the server side one as hackers can modify this.  But if they hackers are setting themselves as admins, I think this is the least of your concern.  Heck if they are doing that, it just makes it easier to catch them.   Anyways, put this code near the top:

AdminList = ["123456789","12121212"];

Now add these event handlers near the bottom:

// Initial JIP check
if (!isDedicated) then {
	PV_EventStatusCheckJIP = [player];
	publicVariableServer "PV_EventStatusCheckJIP";
};

// expects: [status]
// get the status update from the server
if (!isDedicated) then {
	"PV_EventStatusClient" addPublicVariableEventHandler {
		private ["_packet","_status"];
		_packet = _this select 1;
		_status = _packet select 0;
		
		// set the value client side - this value will persist until the client aborts
		Dayz_Epoch_Event_Running = _status;
	};
};

Client Side Continued...

 

Now for the menus.  You will need to know how to override your fn_selfActions.sqf.  If you don't understand look up kryxes' self bloodbag script.  I shamelessly rip his menu logic off in these examples.   This is how we learn, so long as we give credit its ok.

 

open fn_selfActions.sqf and add this near the top.  This is our admin check:

_isAdmin = (getPlayerUID player in AdminList);

Now somewhere around there we are going to be adding some menus to handle the admin and player event menus.  The admin menu will only show if a player has the playeruid you specified in your AdminList variable in init.sqf and the server_functions.sqf.  As I said earlier, if players are spoofing this you got bigger problems.  Thankfully battleye records who sends what PV's and we also log who says they turned the event on.

// admin menu
if (_isAdmin) then {
    if((speed player <= 1)) then {
        if (s_player_admin_option < 0) then {
			s_player_admin_option = player addaction[("<t color=""#E65C00"">" + ("Admin Menu") +"</t>"),"event\AdminMenu.sqf","",5,false,true,"", ""];
        };
    } else {
        player removeAction s_player_admin_option;
        s_player_admin_option = -1;
    };
};

// player TP menu
if((speed player <= 1) && Dayz_Epoch_Event_Running) then {
	if (s_player_event_option < 0) then {
		s_player_event_option = player addaction[("<t color=""#FF0000"">" + ("-- EVENT ACCESS --") +"</t>"),"event\PlayerMenu.sqf","",5,false,true,"", ""];
	};
} else {
	player removeAction s_player_event_option;
	s_player_event_option = -1;
};

Client Side Continued More...

 

We need to create a folder called "events" inside your mission folder.  You can call it what you like but for this example, we are calling it "events".  Create two text files inside name them "AdminMenu.sqf" and "PlayerMenu.sqf"

 

Add the following to AdminMenu.sqf:

private ["_isAdmin"];

_isAdmin = (getPlayerUID player in AdminList);

// quit our script if useris not an admin
if (!_isAdmin_) exitWith {};

AdminStartEvent = {
	Dayz_Epoch_Event_Running = true;
	[] call AdminSendStatus;
};

AdminStopEvent = {
	Dayz_Epoch_Event_Running = false;
	[] call AdminSendStatus;
};

// handle sending event to the server
AdminSendStatus = {
	PV_EventSetStatus = [player, Dayz_Epoch_Event_Running];
	publicVariableServer "PV_EventSetStatus";
};

// show the menu
opt_main_admin =
[
	["",false],
		["-- Admin Event Control -- ", [0], "", -5, [["expression", ""]], "1", "0"],
		["Admin Event START", [0], "", -5, [["expression", "[] spawn AdminStartEvent; "]], "1", "1"],
		["Admin Event STOP", [0], "", -5, [["expression", "[] spawn AdminStopEvent; "]], "1", "1"],		
		["", [-1], "", -5, [["expression", ""]], "1", "0"],
		["Exit", [13], "", -3, [["expression", ""]], "1", "1"]
];

showCommandingMenu "#USER:opt_main_admin";

and add this to PlayerMenu.sqf

// bring player to event
TpToEvent = {
	player setPosATL [1234.1,1234.1,0];
};

// show the menu
opt_main_player =
[
	["",false],
		["-- Event Access -- ", [0], "", -5, [["expression", ""]], "1", "0"],
		["TP Me to the Event", [0], "", -5, [["expression", "[] spawn TpToEvent; "]], "1", "1"],
		["", [-1], "", -5, [["expression", ""]], "1", "0"],
		["Exit", [13], "", -3, [["expression", ""]], "1", "1"]
];

showCommandingMenu "#USER:opt_main_player";

Now just adjust your Admin_List if you haven't already and you should be good to go.

 

That's it, we're done.

Link to comment
Share on other sites

just updated Lesson #2.  I'll be going over how to secure your PV's so that we only expose what we need to to the clients connecting to your server in the next elsson.  I will also show you have to put event handlers and functions inside of your server PBO.  This example will cover an admin menu to turn your event on and off, and a system for players to teleport to the event when it is up and running.  Pretty cool right?

Link to comment
Share on other sites

I'm a little stuck here. Trying to launch a snow/color correction scripts for admin scroll menu, that would make all clients execute same script. Idea is not to use JIP, so players can still relog if they want effect to be cleared.

 

this is under !isDedicated in init

"PV_wOriginal" addPublicVariableEventHandler {
execVM "custom\ActionMenu\sky\1.sqf"; //executes color correction code
};

This is accessed via action menu:

["Original", [2],  "", -5, [["expression", format[EXECscript2 ,"11.sqf"]]], "1", "1"],

11.sqf contains:

publicVariable "PV_wOriginal";

Nothing happens at all.

Link to comment
Share on other sites

I'm a little stuck here. Trying to launch a snow/color correction scripts for admin scroll menu, that would make all clients execute same script. Idea is not to use JIP, so players can still relog if they want effect to be cleared.

 

this is under !isDedicated in init

"PV_wOriginal" addPublicVariableEventHandler {
execVM "custom\ActionMenu\sky\1.sqf"; //executes color correction code
};

This is accessed via action menu:

["Original", [2],  "", -5, [["expression", format[EXECscript2 ,"11.sqf"]]], "1", "1"],

11.sqf contains:

publicVariable "PV_wOriginal";

Nothing happens at all.

 

you need to declare the public variable before you call it. (and it looks like it's a zero not an "o" in your 11.sqf)

 

so 11.sqf should be

PV_wOriginal= [];
publicVariable "PV_wOriginal";

also add some logging to your PV code so you can see when its being called and not

simple as

diag_log "my PV has been called";

then look in the arma2oa.rpt clientside for the log entry.

Link to comment
Share on other sites

No matter what I do, I can't get execVM to work. Other scripts I find online are using it perfectly fine, so that's rather confusing.

 

Neither of these works and I am out of ideas:

"PV_wOriginal" addPublicVariableEventHandler {
player execVM "custom\ActionMenu\sky\1.sqf";
};
"PV_wSnow" addPublicVariableEventHandler {
(_this select 1)
execVM "custom\ActionMenu\sky\2.sqf";
};
"PV_wWasteland" addPublicVariableEventHandler {
[_this select 1]
execVM "custom\ActionMenu\sky\3.sqf";
};

Everything else works fine, it's just this part.

I'll see maybe there's another way for admins to force clients to execute color correction files.

Link to comment
Share on other sites

No matter what I do, I can't get execVM to work. Other scripts I find online are using it perfectly fine, so that's rather confusing.

 

Neither of these works and I am out of ideas:

"PV_wOriginal" addPublicVariableEventHandler {
player execVM "custom\ActionMenu\sky\1.sqf";
};
"PV_wSnow" addPublicVariableEventHandler {
(_this select 1)
execVM "custom\ActionMenu\sky\2.sqf";
};
"PV_wWasteland" addPublicVariableEventHandler {
[_this select 1]
execVM "custom\ActionMenu\sky\3.sqf";
};

Everything else works fine, it's just this part.

I'll see maybe there's another way for admins to force clients to execute color correction files.

 

using execVM within a public variable event is bad practice imho, you should rather fire up some functions

Link to comment
Share on other sites

using execVM within a public variable event is bad practice imho, you should rather fire up some functions

agreed. Plus you can re-use functions and we should always strive to build modular re-usable code.

 

 

also, Raymix, mind showing us how you're calling these PV's.  Do you get any logs in battleye?  Are you using any logging at all inside your PV?

Link to comment
Share on other sites

It would also be an idea to mirror this over to the BIS community wiki, too. That deserves an upgrade in readability. Good work :)

 

Zamboni, like the idea but I'm too invested in other projects to edit wikis.  You are free to copy all of the info here over there if you like.

Link to comment
Share on other sites

  • 4 weeks later...

can you help me with my script ?

Im searching for days now but I cant find a solution.

I want to build a roof/hangar that is hideable by a switch, to start or land an aircraft.

for testing purposes BE and Infistar are OFF.


_nearestPanelorBox = nearestObjects [vehicle player, ["Infostand_2_EP1","MAP_phonebox"], 40];
_nearestOne = _nearestPanelorBox select 0;
_nearestGates = nearestObject [_nearestOne, "MAP_SS_hangar"];
_hide = format["{  _nearestGates hideObject true ;} forEach playableUnits;"];
_hide2 = format["{  _nearestGates hideObject false ;} forEach playableUnits;"];
_inMotion = _nearestOne getVariable ["inMotion",0];


if (_inMotion == 0) then {

_nearestOne setVariable ["inMotion", 1, true];


sleep 0.1;
player setVehicleInit _hide;
sleep 0.1;
processInitCommands;
sleep 0.1;
clearVehicleInit player;


rcrHideRoof = [player,_nearestGates];
publicVariable "rcrHideRoof";


} else {

_nearestOne setVariable ["inMotion", 0, true];
sleep 0.1;


player setVehicleInit _hide2;
sleep 0.1;
processInitCommands;
sleep 0.1;
clearVehicleInit player;

rcrUnHideRoof = [player, _nearestGates];
publicVariable "rcrUnHideRoof";


};

this is my script activated from an addaction call at the fn_selfactions.sqf

 

for the executing player the script works BUT only for him.

 

 

this is what i have in my init.sqf :

if (isDedicated) then {

"rcrHideRoof" addPublicVariableEventHandler {

_packet = _this select 1;
_nearestGates = _packet select 1;
_player = _packet select 0;

_hide = format["{  _nearestGates hideObject true ;} forEach playableUnits;"];

sleep 0.1;
_player setVehicleInit _hide;
sleep 0.1;
processInitCommands;
sleep 0.1;
clearVehicleInit _player;

};

"rcrUnHideRoof" addPublicVariableEventHandler {

_packet = _this select 1;
_nearestGates = _packet select 1;
_player = _packet select 0;

_hide = format["{  _nearestGates hideObject false ;} forEach playableUnits;"];

sleep 0.1;
_player setVehicleInit _hide;
sleep 0.1;
processInitCommands;
sleep 0.1;
clearVehicleInit _player;

};


};

but it wont work somehow :(

Link to comment
Share on other sites

  • 5 months later...

Thank you sir, I learned a lot today, I now know how to cutText to the cursorTarget ^^!

 

However I think there is some mistakes in your code, lesson2:

// server side check, make sure only the server runs this code
if (isDedicated) then {

    // DEBUG: Manually set our event to running... You would normally do this thru a menu etc.
    // note, that this variable is only set on the server itself, so the client has no idea of the value
    // if you wanted, you could pass this thru instead of a skin but that's out of the scope of this tutorial
    Server_Event = true;

    // expects: [player]
    // Handles our client JIP requests
    "PV_JIP_Event_Check" addPublicVariableEventHandler {
        private ["_packet","_player","_owner","_skinClassName"];
        // deserialize our packet
        _packet = _this select 0; // shouldn't be select 1 according to lesson1?     <=================== HERE
        _player = _packet select 0;
        _owener = owner player; // wrong var name <=================== AND HERE
        
        _skinClassName = "FR_GL"; // my personal skin
        
        // check to see if Server_Event has been set by an admin/script.
        if (!isNil "Server_Event") then {
            // validate Server_Event is true, if true, our even is running
            if (Server_Event) then {
                // our event is running, change skin of JIP player
                PV_ChangePlayerSkin = ["FR_GL"];
                _owner publicVariableClient "PV_ChangePlayerSkin";    
            };
        };
    };
};

I know this is not big mistakes, but if someone copy/paste to give it a try, he might encounter some issues ^^

Link to comment
Share on other sites

Thank you sir, I learned a lot today, I now know how to cutText to the cursorTarget ^^!

 

However I think there is some mistakes in your code, lesson2:

// server side check, make sure only the server runs this code
if (isDedicated) then {

    // DEBUG: Manually set our event to running... You would normally do this thru a menu etc.
    // note, that this variable is only set on the server itself, so the client has no idea of the value
    // if you wanted, you could pass this thru instead of a skin but that's out of the scope of this tutorial
    Server_Event = true;

    // expects: [player]
    // Handles our client JIP requests
    "PV_JIP_Event_Check" addPublicVariableEventHandler {
        private ["_packet","_player","_owner","_skinClassName"];
        // deserialize our packet
        _packet = _this select 0; // shouldn't be select 1 according to lesson1?     <=================== HERE
        _player = _packet select 0;
        _owener = owner player; // wrong var name <=================== AND HERE
        
        _skinClassName = "FR_GL"; // my personal skin
        
        // check to see if Server_Event has been set by an admin/script.
        if (!isNil "Server_Event") then {
            // validate Server_Event is true, if true, our even is running
            if (Server_Event) then {
                // our event is running, change skin of JIP player
                PV_ChangePlayerSkin = ["FR_GL"];
                _owner publicVariableClient "PV_ChangePlayerSkin";    
            };
        };
    };
};

I know this is not big mistakes, but if someone copy/paste to give it a try, he might encounter some issues ^^

 

 

good catch.  always nice to have code review.  I'll edit the samples so they are correct

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...