Jump to content
🛡️FORUMS ARE IN READ-ONLY MODE Read more... ×
thy_

Any amount of AI Helicopters provide transport for players of the same side

Recommended Posts

Oi. 

 

I need help with: 
To set correctly that Global Var "TRANSP_GROUP", using a Namespace that doesn't compromise other players using the same Air transport service!

What my script already does on single-player:

Once the player requests the service, the server creates and sends a helicopter to the player's position. As soon as the helicopter lands over there, the player provides what position they wanna go. After the helicopter delivery, it returns to base and it's deleted, ending that service.

Below is what happens when a player gets in the helicopter (pretty sure it will not work in MP if more than one player group requests the service to the server.

// IMPORTANT: code running only on server!

openMap [true, false];

// Defining the server AI group that will provide the support with its helicopter:
TRANSP_GROUP = _supportGrp;

// The coordenates that some side player will provide, clicking over their map:
TRANSP_COORDS = [];

// I would like to specify this event is attached ONLY to the player-leader of _grpAskedResrc group:
addMissionEventHandler ["MapSingleClick", {
  
    // Not sure if it can help me here:
    params ["_units", "_pos", "_alt", "_shift"];
  
    // I'm consern because another player in other part of the map can be requesting Transport too and this global var "TRANSP_GROUP" brings a mess:
    private _supportGrp = missionNamespace getVariable "TRANSP_GROUP";
  
    // Defining the coordenates provided by some side player using this Transport resource:
    TRANSP_COORDS = (findDisplay 12 displayCtrl 51) ctrlMapScreenToWorld getMousePosition;
  
    private _wp = _supportGrp addWaypoint [TRANSP_COORDS, 0];
    _wp setWaypointType 'MOVE';
    _supportGrp setCurrentWaypoint _wp;
    removeMissionEventHandler ["MapSingleClick", _thisEventHandler];
    openMap false;
    vehicle leader _supportGrp vehicleChat "Let's go!";
}];

// Wait until the helicopter gets the coordenates:
waitUntil {
    sleep 1;
    TRANSP_COORDS isNotEqualTo [];
};

systemChat "IT'S WORKING!";

 

I tried to setVariable to the group (namespace) to "isolate" that var changes from other groups but it didn't work.

  • Like 1

Share this post


Link to post
Share on other sites
12 hours ago, thy_ said:

 

I tried to setVariable to the group (namespace) to "isolate" that var changes from other groups but it didn't work.

So you're trying to pass data to/from the separate scopes while avoiding global vars?

 

Use local variables in your "controller" script (the one which adds the eventhandler), pass the required values as the "arguments" parameter of addMissionEventhandler to have them available in the eventhandler's scope as _thisArgs

 

If you need to return a value from the eventhandler you can try this, I'm 90% sure it maybe works... Edit: Tested it, it definitely works.

 

In your controller script:

_data = createHashMap;

addMissionEventhandler [
	"MapSingleClick",
	{
		_thisArgs params ["_returnData"];
		_returnData set ["some key", "some value"]; //Return whatever you need
	},
	[ _data] //Important 
];

waitUntil { count _data > 0 };

systemChat "map clicked!";

Here's a complete example with comments showing a simple teleport feature demonstrating the technique where the event handler returns the position to the calling script appropriately.

Spoiler

// Create our return data reference. 
// I'm using a hashmap for ease of use but a regular array would also work. 
// It just needs to be a reference type.
private _data = createHashMap;
private _t = time + 30; // Just a timeout

openMap [true, true]; // Force the map open

// Add the eventhandler
private _eh = addMissionEventHandler[
  "MapSingleClick",
  {
    params ["_units", "_pos", "_alt", "_shift"];
    _thisArgs params ["_returnData"];

    _returnData set ["pos", _pos]; // The info we want to return
    _returnData set ["done", true]; // Let the calling scope know we're done
  },
  [_data]
];

// Better approach is to wait for a specific value to be set, this way you can set multiple values in the hashmap before returning.
// Note nothing is stopping you from doing other work prior to this waitUntil
waitUntil {_data getOrDefault ["done", false] || time > _t };

// Cleanup
removeMissionEventHandler ["MapSingleClick", _eh];

// Apply the result
private _pos = _data get "pos";
player setPos _pos;
systemChat "Teleported!";

openMap [false, false]; // Close map and enable normal map behaviour

 

 

Ofc working with MP you need to think about the locality of your operations; server vs client, who does what, what is the command requirements (LA / LE / GA / GE).

 

No code formatting cause lousy options on phone

Edited on PC.

  • Like 1
  • Thanks 1

Share this post


Link to post
Share on other sites

A question:


How do I force addMissionEventHandler to consider only the caller group leader (always a player)?

_data = createHashMap;

addMissionEventhandler [
	"MapSingleClick",
	{
		_thisArgs params ["_returnData"];
		_returnData set ["some key", "some value"]; //Return whatever you need
	},
	[ _data] //Important 
];

waitUntil { count _data > 0 };

systemChat "map clicked!";

PS: I loved this way of "extracting" data from an EH.

Share this post


Link to post
Share on other sites
On 1/11/2025 at 9:28 PM, thy_ said:

Below is what happens when a player gets in the helicopter (pretty sure it will not work in MP if more than one player group requests the service to the server.

 

I tried to setVariable to the group (namespace) to "isolate" that var changes from other groups but it didn't work.

 

setVariable is fine. you can set one on helicopter when busy for a group. Don't forget the public parameter in MP (helo setVariable ["busy",TRUE,TRUE])
furthermore, you can pass variable like coordinates grp1 setVariable ["destination",_coordsOnClick,TRUE]   as well.

 

taxi module (SP/MP):

 

just in case of you're looking for:
- possibility for choosing an available taxi (vehicle or helo) sorted by distance;
- possibility for altering destination;
- pick-up / deploy units on sharp hill or at sea;

- asking for the  taxi to wait for the group or return back to base.

  • Like 2

Share this post


Link to post
Share on other sites
12 hours ago, thy_ said:

How do I force addMissionEventHandler to consider only the caller group leader (always a player)?

 

The command has local effect so run it only when leader player == player.

  • Like 1

Share this post


Link to post
Share on other sites
17 hours ago, pierremgi said:

just in case of you're looking for:
- possibility for choosing an available taxi (vehicle or helo) sorted by distance;
- possibility for altering destination;
- pick-up / deploy units on sharp hill or at sea;

- asking for the  taxi to wait for the group or return back to base.

 

I'm building one independently because it will integrate natively the CSWR script. Thanks anyway!

Share this post


Link to post
Share on other sites
12 hours ago, mrcurry said:

 

The command has local effect so run it only when leader player == player.

 

Do you mean: should I do a remoteExec to add the addMissionEventHandler for the leader of the caller group? I don't think I got it because I wouldn't even know how to do that. 

 

Because if EventHandlers are local, it means it's running just on server once my entire script just use the server side.

 

EDITED: forget it. I found the Larrow solution, working the addMissionEventHandler through remoteExec 😉.

Share this post


Link to post
Share on other sites

But how to get back the information that the leader-player clicked on their map? Storing in a var, is it enough?

_dataPlus = [] remoteExec ["THY_fnc_CSWR_TRANSPORT_caller_pos", leader _grpCaller];


This solution is working for single-player, but not sure for multiplayer. What do you think?

 

Server-side:

Spoiler

THY_fnc_CSWR_TRANSPORT_provide_pos_using_map = {
	// This function requests for the player where they want to go, and defines the position through the helicopter waypoint.
	// Returns _data: hashMap/array. If returns an empty array, it's coz something went wrong.

	params ["_data", "_grpCaller", "_grpVeh", "_veh", "_basePos", "_minDisCall"];
	private [...];

	// Initial values:
	_dataPlus   = createHashMap;
	
	while { conditions } do {

		// For Single-player:
		if !isMultiplayer then {
			_dataPlus = [] call THY_fnc_CSWR_TRANSPORT_caller_pos;
		
		// For Multiplayer:
		} else {
			_dataPlus = [] remoteExec ["THY_fnc_CSWR_TRANSPORT_caller_pos", leader _grpCaller];
		};

		// Waiting for caller to provide position:
		waitUntil { sleep 1; _dataPlus getOrDefault ["isDone", false] };
		
		// Validations
	};
	
	// Return:
	_data;
};

 

 

Client-side:

THY_fnc_CSWR_TRANSPORT_caller_pos = {
	// CLIENT-SIDE FUNCTION called by THY_fnc_CSWR_TRANSPORT_provide_pos_using_map.
	// This function asks for the leader of caller group its cursor position over the map by click.
	// Returns _dataPlus: hashMap/array. If empty ?????????????

	//params ["", "", "", ""];
	private ["_dataPlus"];

	// Initial values:
	_dataPlus = createHashMap;
	// Escape:
	if ( !alive player || player isNotEqualTo leader (group player) ) exitWith { _dataPlus };
	// Forcing player to open map:
	openMap [true, true];
	// Check the click pos on map:
	addMissionEventHandler ["MapSingleClick",{
			params ["_units", "_pos", "_alt", "_shift"];
			// Defining:
			_thisArgs params ["_callerRequestData"];
			_callerRequestData set ["whereToGoPos", _pos];
			_callerRequestData set ["isDone", true];
			// Closes the player map normally:
			openMap [false, false];
			// Ends the event handler:
			removeMissionEventHandler ["MapSingleClick", _thisEventHandler];
		},
		// Return > Where _thisArgs stores to be used out of EH scope:
		[_dataPlus]
	];

	// Waiting for caller to provide position:
	waitUntil { sleep 1; _dataPlus getOrDefault ["isDone", false] };

	// Return:
	_dataPlus;
};

Share this post


Link to post
Share on other sites

Why do you remoteExec THY_fnc_CSWR_TRANSPORT_caller_pos ? This function can be everywhere.
the MEH "MapSingleClick" is local, so you just have to broadcast some data when a player click, and is easy with setVariable. For example, when a player has clicked:
player setVariable ["posClicked",_pos,TRUE];  (_pos is the clicked position). You can change player by leader player, group player, just avoid missionNameSpace if you need multiple positions, not overridden by the last click. In MP, if you run locally object setVariable ["blahblah",data,TRUE]; all PCs get the data on this shared object (played unit,AI unit, vehicle, crate...). Just be sure the object exists on each PC (which is the case for any played unit.

 

In other words, run your MEH everywhere with interface, then add conditions on code if required (side or else), then broadcast results by variable.

You can even send code like: player setVariable ["test", hint "woaw",TRUE]; (hint is LE, but run everywhere by this public variable; player is the local player but also the shared unit known everywhere as other objects)

 

Keep remote execution for Argument or Effect locality in commands (and functions, depending on called commands)

 

Never abuse of remote execution but also public variables (public setVariable or publicVariable command). Broadcast at minimum.

Multiplayer_Scripting

  • Like 1

Share this post


Link to post
Share on other sites

To make clear, all these functions are in the file named CSWR_globalFunctions.sqf where its "access" is through THY_CSWR_functions.

 

That said, the header of the CSWR_globalFunctions.sqf has NO restrictions about hasInterface or isServer because - may I be wrong - only the server can read these CSWR_globalFunctions.sqf functions...

 

description.ext:

Spoiler

class cfgFunctions {
	#include "CSWR\THY_CSWR_functions.hpp"
};

 

THY_CSWR_functions.hpp:

Spoiler

class THY_CSWR_functions {
	tag = "THY";
	class CSWR {
		file = "CSWR";
		//...
		class CSWR_globalFunctions {
			preInit = 1;
		};
		//...
	};
};

 

So, when I listen to someone much more experienced advise me literally to use the player command in a multiplayer context (yeap, multiple positions could be provided simultaneously), it makes a mess in my mind, "forcing" to use remoteExec hehe. Okay, got it about setVariable being more "straight" to the point:

 

14 hours ago, pierremgi said:

(...) You can change player by leader player, group player, just avoid missionNameSpace if you need multiple positions (...) run your MEH everywhere with interface, then add conditions on code if required (side or else), then broadcast results by variable. (...)

 

Do you mean... something like this below? Again, in my understanding, I cannot even see the possibility of a player machine reading this function to make the hasInterface needed.

Spoiler

THY_fnc_CSWR_TRANSPORT_provide_pos_using_map = {
    
    //...
    
    // Just for the caller group leader:
    if ( hasInterface && alive player && player isEqualTo leader _grpCaller ) then {
        _dataPlus = [] call THY_fnc_CSWR_TRANSPORT_caller_pos;
    };

    // ...
};

 

An honest question by something thinking hard: this solution below (transferring data by hashMap) is that bad, or, worst, doesn't make sense?

 

I'm here to learn with you, guys... so bring it on hehe!

 

Both functions in  CSWR_globalFunctions.sqf:

Spoiler

THY_fnc_CSWR_TRANSPORT_provide_pos_using_map = {
	// Returns _data: hashMap/array. If returns an empty array, it's coz something went wrong.

	params ["_data", "_grpCaller", "_grpVeh", "_veh", "_basePos", "_minDisCall"];
	private [...];

	_dataPlus = createHashMap;
	
	while { alive _veh && alive driver _veh && alive leader _grpCaller} do {
		
		// Just for caller group leader machine:
		_dataPlus = [] call THY_fnc_CSWR_TRANSPORT_caller_pos;
      
		// Waiting for caller to provide position:
		waitUntil { sleep 1; _dataPlus getOrDefault ["isDone", false] };
		
		// Validations 
		// Approved to leave the loop:
		if ... then { break };
	};
	
	// Return:
	_data;
};

 

 

Spoiler

THY_fnc_CSWR_TRANSPORT_caller_pos = {
	// Returns _dataPlus: hashMap/array.

	//params [];
	private ["_dataPlus"];

	// Initial values:
	_dataPlus = createHashMap;
  
	// Escape:
	if ( !alive player || player isNotEqualTo leader (group player) ) exitWith { _dataPlus };
  
	// Forcing player to open map:
	openMap [true, true];
  
	// Check the click pos on map:
	addMissionEventHandler ["MapSingleClick",{
			params ["_units", "_pos", "_alt", "_shift"];
      
			// Defining:
			_thisArgs params ["_callerRequestData"];
			_callerRequestData set ["whereToGoPos", _pos];
			_callerRequestData set ["isDone", true];
      
			// Closes the player map normally:
			openMap [false, false];
			// Ends the event handler:
			removeMissionEventHandler ["MapSingleClick", _thisEventHandler];
		},
		// Return:
		[_dataPlus]
	];
  
	// Waiting for caller to provide position:
	waitUntil { sleep 1; _dataPlus getOrDefault ["isDone", false] };
  
	// Return:
	_dataPlus;
};

 

@pierremgi, I am trying but I cannot see the north you provided in my code. Can you post here a few lines of the change?

Share this post


Link to post
Share on other sites
On 1/14/2025 at 9:28 PM, thy_ said:

But how to get back the information that the leader-player clicked on their map? Storing in a var, is it enough?

 

Broadly speaking you can either use a shared data container like @pierremgi suggested, ( publicVariable or setVariable with broadcast: true ), he explained it so I won't.

 

The other option is to implement server query and client response functions and use remoteExec to handle the comms. It's more complex but encapsulates the code well.

To do this split the procedure into 3 parts: 

  1. fnc_server_query_destination - Runs only on server. Does prep and remoteExecs function 2. on the client.
  2. fnc_client_select_destination_and_respond - Here the client takes over. This runs only on client with interface. This is where you use the previous trick with the "MapSingleClick" eventhandler to fetch the result. Then the client remoteExecs function 3. back onto the server.
  3. fnc_server_handle_response - The server takes over again and applies the changes i.e. makes the helo move and what not.

The remoteExec calls in this solution need to be sent only between the machines involved, do not use 0 as target.

 

14 minutes ago, thy_ said:

 

An honest question by something thinking hard: this solution below (transferring data by hashMap) is that bad, or, worst, doesn't make sense

 

That will not work as is, no. remoteExec is fire-and-forget so cannot return a value.

 

I just want to clarify that there are two parts to the problem we're talking about:

  1. Getting the selected position from the eventhandler to the calling script on client's machine.
  2. Getting the selected position from the client to the server.

1. can be solved with the hashmap-trick

or

using setVariable on the player 

 

2. can be solved with the server-client response pattern

or

using publicVariable

or

setVariable on the player with broadcast: true 

 

There are no right answers to which approach to take, choose whichever you feel you can pull off.

 

If you need more examples just say which method you'd prefer.

  • Like 2

Share this post


Link to post
Share on other sites
1 hour ago, thy_ said:

That said, the header of the CSWR_globalFunctions.sqf has NO restrictions about hasInterface or isServer because - may I be wrong - only the server can read these CSWR_globalFunctions.sqf functions...

CfgFunctions are always available too all, preinit functions will be called automatically on all machines.

You should avoid execution of code that may or may not exist on the other machine. Only execute that which you know exists 

 

As a side note: Just so you are aware, defining the final function variables on your own like that means you do not get all the features of the functions library, like compileFinal or pre-preinit definition. Here's a not so organised example how to get full functionality.

 

Share this post


Link to post
Share on other sites

Sorry, I don't have a clear understanding on how you manage your functions, CSWR or else. My bad.

Don't forget initialization order.

For example, there is no interest for calling function by preinit (then running at start) when you may run it in game.Furthermore, if you run with player command, so when the player is player.

what about postInit?

 Probably, some functions should be called from initPlayerLocal.sqf or from an event script waiting for locality , then starting by waitUntil {!isNull player}; in scheduled scope of course.

Have fun.

 

Share this post


Link to post
Share on other sites
1 hour ago, mrcurry said:

(...) remoteExec is fire-and-forget so cannot return a value.

 

Thanks. So, in this case, following the @pierremgi said, this is the new try and looks it's working finally:

 

Both in CSWR_globalFunctions.sqf:

THY_fnc_CSWR_TRANSPORT_provide_pos_using_map = {
    // Returns _data: hashMap/array.

    params ["_data", "_grpCaller", "_grpVeh", "_veh", "_basePos", "_minDisCall"];
    private [...];

    // Initial values:
    _requestedPos = [];
	
    // Main function:
    while { alive _veh && alive driver _veh && alive leader _grpCaller} do {
        // Internal initial values:
        leader _grpCaller setVariable ["whereToGoPos", [], true];
      
        // For caller group leader's machine:
        _requestedPos = [_grpCaller] call THY_fnc_CSWR_TRANSPORT_caller_pos;
      
        // Waiting for caller to provide position:
        waitUntil { sleep 1; !alive leader _grpCaller || _requestedPos isNotEqualTo [] };
      
        // Validations
        // code...
		
        // Approved to leave the loop:
        if (conditions...) then { break };
	};
	
    // Code...
	
    // Preparing to return:
    _data set ["whereToGoPos", _requestedPos];
	
    // Return:
    _data;
};
THY_fnc_CSWR_TRANSPORT_caller_pos = {
    // Returns _pos: array. Positon where the caller group leader wants to go.

    params ["_grpCaller"];
    private ["_pos"];

    // Making sure player exist in multiplayer:
    waitUntil { sleep 0.1; !isNull player };
    
    // Initial values:
    _pos = [];
  
    // Escape > If the machine has no interface, or the player is dead, or the player is not the caller group leader, abort:
    if ( !hasInterface || !alive player || player isNotEqualTo leader _grpCaller ) exitWith { _pos };
	
    // Forcing player to open map:
    openMap [true, true];
	
    // Check the click pos on map:
    addMissionEventHandler [
        "MapSingleClick", {
            params ["_units", "_pos", "_alt", "_shift"];
			
            // Defining:
            player setVariable ["whereToGoPos", _pos, true];
			
            // Closes the player map normally:
            openMap [false, false];
          
            // Ends the event handler:
            removeMissionEventHandler ["MapSingleClick", _thisEventHandler];
        }
    ];
   
    // Waiting for caller to provide position:
    waitUntil { sleep 1; !alive player || (leader _grpCaller getVariable "whereToGoPos") isNotEqualTo [] };
	
    // prepare to return:
    _pos = leader _grpCaller getVariable "whereToGoPos";	
	
    // Return:
    _pos;
};

 

  • Like 1

Share this post


Link to post
Share on other sites

×