瀏覽代碼

Updating scripts to latest along with python update

Kleinburger 7 年之前
父節點
當前提交
42d0bb39b8

+ 3 - 0
.gitignore

@@ -29,3 +29,6 @@
 # Executables
 *.out
 *.app
+
+#Temp Folder
+Missions/Temp/*

+ 4 - 0
Scripts/init.sqf

@@ -5,6 +5,7 @@
 //	Change By: CC Cruisie
 //  Change Log:
 //	1-16-2018 - Correct Script Path for CR
+//  1-31-2018 - Added SCFramework support - CC Klein
 //
 //  ADD THIS INTO THE MISSION FOLDER, NEXT TO MISSION.SQM
 //
@@ -22,6 +23,9 @@ _EndSplashScreen = {
 //////////LAAT C Monitor/////////////
 [] execVM "scripts\LAATCMonitor.sqf";
 /////////////////////////////////////
+//////////SCFramework////////////////
+[] execVM "scripts\SCFramework\Init_SCFramework.SQF";
+/////////////////////////////////////
 ///////// Infatry Loadouts /////////
 // loadOutbox = Variable Name - A box/object - must have object down to work
 loadOutbox addAction ["Clone Recruit Loadout", "scripts\loadouts\infatry\loadout_cr.sqf"];

+ 41 - 0
Scripts/scripts/SCFramework/Config_SCFramework.SQF

@@ -0,0 +1,41 @@
+/*
+	Config_SCFramework.SQF
+	
+	Configuration options for BDC's Server-Client Framework
+	
+	Written for CO18 Resistance Plus by ^bdc	May 2017
+	Re-written for dedicated server missions and Zeus compatibility		Jan 2018
+*/
+
+// Configurable global variables
+BDC_SCFramework_LoggingFreq = 300; // Timer, in seconds, that a log file on local AI ownership (cached including) as well as FPS is added (0 to disable) (default: 300)
+BDC_SCFramework_LoggingDelay = 60; // Delay, in seconds, prior to starting logging loop (default: 60)
+BDC_SCFramework_GroupManagerEnable = true; // Enable/disable automaticm, periodic empty group deletion function (default: true, unless another function is already setup in mission to do this)
+BDC_SCFramework_ClientRetainVehOwnership = true; // If true, when player exits a vehicle, as the last person in it, will retain vehicle ownership (via EH called from client to server)
+BDC_SCFramework_DetailedLogging = false; // If enabled, logging will occur from every eventhandler and function called (Used primarily for testing and initial setup) (default: false)
+BDC_SCFramework_ServerFPSReport_KeyCodes = [0x3B,"F1"]; // Array of two values: [(raw dikCode),"Press x Key for Server FPS Report"] - Example: [0x3B,"F1"]
+BDC_SCFramework_ServerFPSReport_AdminUIDList = [ // List of player UID's for admins in string form (used only for the Server FPS Report)
+	"76561198033061067","76561197980091303","76561198063946636","76561198006790310","76561198065200898","76561198128186617","76561197998298758","76561198068431012","76561198002887102"
+	];
+BDC_SCFramework_ServerFPSReport_ModulesNames = [ // Short Names/Acronyms (in string format) of AI modules the server and each successive headless client control, starting from 1 (used purely for display purposes in the ServerFPSReport and relates to specificAITable only)
+	// Order goes: Server,HC1,HC2,HC3,HC4,HC5 -- If using no SpecificAITable, then set all 6 to be blank to prevent a display error, ex: "","","","","",""
+	// "HQ AI","Marker Vehicles","","","",""   // example, if using only two headless clients and moving specific AI to each one
+	"","","","",""
+	];
+BDC_SCFramework_HCOffloading_Enable = true; // Master enable/disable flag for having the server automatically offload AI groups (either automatically or specifically) to headless client(s)
+BDC_SCFramework_HCOffloading_StartDelay = 120; // Start delay in seconds before starting (to allow all HC's to connect and collect ID's) (default: 60)
+BDC_SCFramework_HCOffloading_Frequency = 5; // Timer, in seconds, that server checks allGroups for possible offloading (default: 5)
+BDC_SCFramework_HCOffloading_GroupTimerMinimum = 10; // Timer in seconds that a group must exist prior to any offloading occurring (default: 5 - to allow for unit "painting", loadouts, and skill-set scripts to finish) 
+BDC_SCFramework_HCOffloading_SpecificAITable_Enable = true; // Enable/disable pre-configured AI groups to move to specific headless clients per AITable below
+BDC_SCFramework_HCOffloading_SpecificAITable = [ // List of Specific AI groups (flagged by specific variables) that offload to specific headless clients (by number; not by name) - (group MUST be spawned in by Fnc_SCFramework_CreateGroup)
+	// ["ModuleName (txt field for RPT log reference)","Specific boolean variable name attached to Group during creation",HeadlessClientID assigned by SCFramework (1-5)]
+	//["AI Module","AIModule_SpawnedGroup",2] - Examples listed below
+	//["HQ AI","HQAI",1], // Theoretical AI at HQ, created by Fnc_SCFramework_CreateGroup, with variable "HQAI" attached to flag all related groups, transferred to HC #1
+	];
+BDC_SCFramework_HCOffloading_AutomaticOffloading_Enable = false; // Enable/disable automatic offloading, irrespective of specific configured groups, to be offloaded to headless clients - Note: Automatic offloading occurs AFTER specific offloading has run
+BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Enable = true; // Enable/disable the automatic, timed transfer of Zeus-spawned in AI groups (player-side, locally owned AI that are NOT part of the player group) ownership to server and putting them into the HC offloading pool (default: true)
+BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Frequency = 180; // Number of seconds client-side monitor checks for locally-owned AI (presumably spawned by Zeus Op) (default: 60 to 300 to allow time for waypoints and the like to be made)
+BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_FlagOffloadedGroups = true; // If enabled, any initially-local, Zeus-owned groups (presumably spawned by Zeus Op) can only be automatically transferred to server once; if forcibly moved back to a Zeus op, they will be flagged unable to be automatically offloaded again, therefore will have to be moved manually to the server via addon
+BDC_SCFramework_HCOffloading_AutomaticOffloading_MaxAIPerHeadlessClient = 100; // Target number of maximum AI we want on each headless client (when this number is met or slightly exceeded, routine seeks to offloading AI to next HC in the list) (there is no default for this; this must be tested for performance on each setup although I recommend 100 to start)
+
+diag_log format["Config_SCFramework.SQF: Eventhandlers, functions, and global vars loaded."];

+ 136 - 0
Scripts/scripts/SCFramework/Fnc_ServerFPSReport.SQF

@@ -0,0 +1,136 @@
+/*
+	Fnc_ServerFPSReport.SQF
+	
+	An on-the-fly server and headless client FPS report that sends information on owned AI and vehicles
+	as well as any of the two that might have cached ones
+	
+	Check Config_SCFramework for configuration options
+*/
+
+diag_log format["(SCFramework) ServerFPSReport Script started."];
+
+// Action and default global var
+s_player_FPSReport = -1;
+ServerFPSReport_Enabled = false;
+
+SCFramework_DisplayFPSReport = {
+	_DoLoop = true;
+	while {_DoLoop} do {
+		ServerFPSReport_Received = [];
+		SCFramework_ServerFPSRequest = SCFramework_ClientID;
+		publicVariable "SCFramework_ServerFPSRequest";
+		sleep 1; // Wait for response (Server_SCFramework)
+			// Re-organize
+		_SArray = [0,0,0,0,0,0];
+		_HC1Array = [0,0,0,0,0,0];
+		_HC2Array = [0,0,0,0,0,0];
+		_HC3Array = [0,0,0,0,0,0];
+		_HC4Array = [0,0,0,0,0,0];
+		_HC5Array = [0,0,0,0,0,0];
+		{
+			_ProcessArray = _x;
+			_Name = _ProcessArray select 0;
+			switch (_Name) do {
+				case "Server": { _SArray = _ProcessArray; };
+				case "HC1": { _HC1Array = _ProcessArray; };
+				case "HC2": { _HC2Array = _ProcessArray; };
+				case "HC3": { _HC3Array = _ProcessArray; };
+				case "HC4": { _HC4Array = _ProcessArray; };
+				case "HC5": { _HC5Array = _ProcessArray; };
+			};
+		} forEach ServerFPSReport_Received;
+			// Create main array to be split up
+		ServerFPSReport_Received = [_SArray,_HC1Array,_HC2Array,_HC3Array,_HC4Array,_HC5Array];
+			// Gather module names
+		_SrvName = "";
+		_HC1Name = "";
+		_HC2Name = "";
+		_HC3Name = "";
+		_HC4Name = "";
+		_HC5Name = "";
+		if (BDC_SCFramework_ServerFPSReport_ModulesNames select 0 != "") then {
+			_SrvName = format["\n%1",BDC_SCFramework_ServerFPSReport_ModulesNames select 0];
+		};
+		if (BDC_SCFramework_ServerFPSReport_ModulesNames select 1 != "") then {
+			_HC1Name = format["\n%1",BDC_SCFramework_ServerFPSReport_ModulesNames select 1];
+		};
+		if (BDC_SCFramework_ServerFPSReport_ModulesNames select 2 != "") then {
+			_HC2Name = format["\n%1",BDC_SCFramework_ServerFPSReport_ModulesNames select 2];
+		};
+		if (BDC_SCFramework_ServerFPSReport_ModulesNames select 3 != "") then {
+			_HC3Name = format["\n%1",BDC_SCFramework_ServerFPSReport_ModulesNames select 3];
+		};
+		if (BDC_SCFramework_ServerFPSReport_ModulesNames select 4 != "") then {
+			_HC4Name = format["\n%1",BDC_SCFramework_ServerFPSReport_ModulesNames select 4];
+		};
+		if (BDC_SCFramework_ServerFPSReport_ModulesNames select 5 != "") then {
+			_HC5Name = format["\n%1",BDC_SCFramework_ServerFPSReport_ModulesNames select 5];
+		};
+			// Create vars for display
+		_SA = ServerFPSReport_Received select 0;
+			_SFPS = _SA select 1; _SOAI = _SA select 2; _SCAI = _SA select 3; _SLAI = (_SOAI - _SCAI); _SOV = _SA select 4; _SCV = _SA select 5; _SAV = _SOV - _SCV;
+		_HC1A = ServerFPSReport_Received select 1;
+			_HC1FPS = _HC1A select 1; _HC1OAI = _HC1A select 2; _HC1CAI = _HC1A select 3;  _HC1LAI = (_HC1OAI - _HC1CAI);
+		_HC2A = ServerFPSReport_Received select 2;
+			_HC2FPS = _HC2A select 1; _HC2OAI = _HC2A select 2; _HC2CAI = _HC2A select 3; _HC2LAI = (_HC2OAI - _HC2CAI); 
+		_HC3A = ServerFPSReport_Received select 3;
+			_HC3FPS = _HC3A select 1; _HC3OAI = _HC3A select 2; _HC3CAI = _HC3A select 3; _HC3LAI = (_HC3OAI - _HC3CAI); 
+		_HC4A = ServerFPSReport_Received select 4;
+			_HC4FPS = _HC4A select 1; _HC4OAI = _HC4A select 2; _HC4CAI = _HC4A select 3; _HC4LAI = (_HC4OAI - _HC4CAI);
+		_HC5A = ServerFPSReport_Received select 5;
+			_HC5FPS = _HC5A select 1; _HC5OAI = _HC5A select 2; _HC5CAI = _HC5A select 3; _HC5LAI = (_HC5OAI - _HC5CAI);
+		_TActiveAI = 0;
+		_TActiveAI = _SLAI + _HC1LAI + _HC2LAI + _HC3LAI + _HC4LAI + _HC5LAI;
+		//_TVActive = _HC1AV + _HC2AV + _HC3AV + _HC4AV + _HC5AV;
+		_LocalGroups = 0;
+		_LocalAI = 0;
+		_LocalGroupsStr = "";
+		_LocalGroupsCont = 0;
+		{
+			if (local _x) then { 
+				if (group player != _x && (count units _x > 0)) then {
+					_LocalGroups = _LocalGroups + 1; 
+					_LocalAI = _LocalAI + (count units _x);
+					_ZeusFlagged = _x getVariable ["ZeusFlagged",false];
+					_Str = format["%1 %2\n",_x,side _x];
+					if (_ZeusFlagged) then {
+						_Str = format["%1 %2 (Flagged)\n",_x,side _x];
+					};
+					_LocalGroupsCont = _LocalGroupsCont + 1;
+					if (_LocalGroupsCont == 1) then {
+						_LocalGroupsStr = "\nLocally Owned Groups:\n";
+					};
+					_LocalGroupsStr = _LocalGroupsStr + _Str;
+				};
+			};
+		} forEach allGroups;
+		_AutomaticOffloading = "Enabled";
+		if (!BDC_SCFramework_HCOffloading_AutomaticOffloading_Enable) then {
+			_AutomaticOffloading = "Disabled";
+		};
+		_TransferZeusOpAI = "Enabled";
+		if (!BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Enable) then {
+			_TransferZeusOpAI = "Disabled";
+		};
+			// Display hint
+		hint format [
+			"Server/HC FPS Report:\n\nServer - %2 FPS%1\nAI: %3 Active | %4 Owned | %5 Cached\nVehs: %6 Active | %7 Total | %8 Cached\n\n
+			Headless Client 1 - %10 FPS%9\nAI: %11 Active | %12 Owned | %13 Cached\n\n
+			Headless Client 2 - %15 FPS%14\nAI: %16 Active | %17 Owned | %18 Cached\n\n
+			Headless Client 3 - %20 FPS%19\nAI: %21 Active | %22 Owned | %23 Cached\n\n
+			Headless Client 4 - %25 FPS%24\nAI: %26 Active | %27 Owned | %28 Cached\n\n
+			Headless Client 5 - %30 FPS%29\nAI: %31 Active | %32 Owned | %33 Cached\n\n
+			%34 Total Active Server AI\n\nAutomatic HCOffloading: %35\nAuto-Transfer Zeus-Op AI: %36\n\nLocally Owned Groups: %37 (%38 AI)\n
+			%39",
+			_SrvName,_SFPS,_SLAI,_SOAI,_SCAI,_SAV,_SOV,_SCV,
+			_HC1Name,_HC1FPS,_HC1LAI,_HC1OAI,_HC1CAI,
+			_HC2Name,_HC2FPS,_HC2LAI,_HC2OAI,_HC2CAI,
+			_HC3Name,_HC3FPS,_HC3LAI,_HC3OAI,_HC3CAI,
+			_HC4Name,_HC4FPS,_HC4LAI,_HC4OAI,_HC4CAI,
+			_HC5Name,_HC5FPS,_HC5LAI,_HC5OAI,_HC5CAI,
+			_TActiveAI,_AutomaticOffloading,_TransferZeusOpAI,
+			_LocalGroups,_LocalAI,_LocalGroupsStr
+		];
+		if (!ServerFPSReport_Enabled) then { _DoLoop = false; hint ""; };
+	};
+};

+ 142 - 0
Scripts/scripts/SCFramework/Functions_SCFramework.SQF

@@ -0,0 +1,142 @@
+/*
+	Functions_SCFramework.SQF
+	
+	External file containing useful functions that can be called from server, any headless client, and any player client 
+*/
+
+	// Create group that never returns NULL
+Fnc_SCFramework_CreateGroup = {
+	private ["_Group","_Side","_Variable"];
+	_Side = _this select 0;
+	_Variable = "";
+	if (count _this > 1) then {
+		_Variable = _this select 1;
+	};
+	_RunCreationLoop = true;
+	_Center = createCenter _Side;
+	sleep 0.1;
+	while {_RunCreationLoop} do {
+		_Group = createGroup [_Side,true];
+		if (!isNull _Group) then { 
+			_RunCreationLoop = false; 
+		};
+		sleep 0.1;
+	};
+	if (_Variable != "") then {
+		_Group setVariable [_Variable,true,true];
+	};
+	_Group setVariable ["SpawnTime",time,true]; // Config_SCFramework
+	diag_log format["(SCFramework) Fnc_SCFramework_CreateGroup: Group %1 %2 created.",_Group,_Side];
+	_Group
+};
+
+	// Remove specified element from an array - Returns array
+Fnc_SCFramework_DeleteArrayElement = {
+	_Array = _this select 0;
+	_Element = _this select 1;
+	_Count = 0;
+	{
+		if (_x == _Element) then {	
+			_Array deleteAt _Count;
+		};
+		_Count = _Count + 1;
+	} forEach _Array;
+	_Array
+};
+
+	// Remove duplicate elements and sort array alphabetically
+Fnc_SCFramework_CleanArray = {
+	_Array = _this select 0;
+	_TArray = [];
+	{
+		_TArray pushBackUnique _x;
+	} forEach _Array;
+	_TArray sort true;
+	_Arra = _TArray;
+	_Array
+};
+
+	// Called from any module that needs to spawn a vehicle within a certain radius of a specified point on a road; will increment radius per 100m until suitable road is found and then will return that position
+Fnc_SCFramework_FindRoadPos = {
+	_SpawnPos = _this select 0;
+	_Radius = _this select 1;
+		// Correct if radius is too small
+	if (_Radius <= 100) then { _Radius = 100; };
+	_FoundRoad = false;
+	_RunLoop = true;
+	_SearchRadius = 0;
+	while {_RunLoop} do {
+			// Work in increments of 100
+		_SearchRadius = _SearchRadius + 100;
+		_NearRoads = _SpawnPos nearRoads _SearchRadius;
+		if (count _NearRoads > 0) then {
+			_Road = _NearRoads call BIS_fnc_SelectRandom;
+			_FoundRoad = true;
+			_SpawnPos = getPos _Road;
+			diag_log format["(SCFramework) Fnc_SCFramework_FindRoadPos: Found suitable spawn position on road at %1",_SpawnPos];
+		};
+		if (_FoundRoad) then {
+			_RunLoop = false;
+		};
+		if (_SearchRadius > _Radius) then {
+			_RunLoop = false;
+		};
+		sleep 0.05;
+	};
+	if (!_FoundRoad) then {
+		diag_log format["(SCFramework) Fnc_SCFramework_FindRoadPos: Failed to find road from given position %1 and radius %2. Returning original position.",_SpawnPos,_Radius];
+	};
+	_SpawnPos
+};
+
+	// Completely strip a unit of all of its gear
+Fnc_SCFramework_ClearUnit = {
+	_Unit = _this select 0;
+	_Unit unlinkItem (goggles player);
+	removeAllWeapons _Unit;
+	removeAllItems _Unit;
+	removeAllItemsWithMagazines _Unit;
+	removeVest _Unit;
+	removeHeadgear _Unit;
+	removeBackpack _Unit;
+	removeUniform _Unit;
+	removeGoggles _Unit;
+};
+
+	// Set fuel on a vehicle EH
+"Fnc_SCFramework_SetFuel" addPublicVariableEventHandler {
+	_Array = _this select 1;
+	_Vehicle = _Array select 0;
+	_Fuel = _Array select 1;
+	if (local _Vehicle && alive _Vehicle) then {
+		_Vehicle setFuel _Fuel;
+		diag_log format["(SCFramework) Setting local vehicle %1 %2 fuel to %3.",_Vehicle,typeOf _Vehicle,_Fuel];
+	};
+};
+
+	// Set Fuel, Repair, and Reammo Cargo EH
+"Fnc_SCFramework_SetCargo" addPublicVariableEventHandler {
+	_Array = _this select 1;
+	_Vehicle = _Array select 0;
+	_CargoType = _Array select 1;
+	_CargoAmt = _Array select 2;
+	if (local _Vehicle && alive _Vehicle) then {
+			// Determine cargo type: Fuel = 1, Repair = 2, Reammo = 3
+		switch (_CargoType) do {
+			case 1: { 
+				_Vehicle setFuelCargo _CargoAmt;
+				diag_log format["(SCFramework) Setting local vehicle %1 %2 fuel cargo to %3.",_Vehicle,typeOf _Vehicle,_CargoAmt];
+			};
+			case 2: {
+				_Vehicle setRepairCargo _CargoAmt;
+				diag_log format["(SCFramework) Setting local vehicle %1 %2 repair cargo to %3.",_Vehicle,typeOf _Vehicle,_CargoAmt];
+			};
+			case 3: {
+				_Vehicle setAmmoCargo _CargoAmt;
+				diag_log format["(SCFramework) Setting local vehicle %1 %2 ammo cargo to %3.",_Vehicle,typeOf _Vehicle,_CargoAmt];
+			};
+		};
+	};
+};
+
+diag_log format["Functions_SCFramework.SQF: Functions loaded."];

+ 137 - 0
Scripts/scripts/SCFramework/HeadlessClient_SCFramework.SQF

@@ -0,0 +1,137 @@
+/*
+	HeadlessClient_SCFramework.SQF
+	
+	Script file for any and all headless clients that connect to dedicated server
+	containing eventhandlers and related functions
+*/
+
+if (hasInterface || isServer) exitWith {}; // Headless clients only
+
+// Reset global variables
+SCFramework_HCPingResponseServer = []; // Reset default
+SCFramework_ClientID = nil; // ClientID is the owner number that is used during publicVariableClient calls from the server for offloading AI units/groups
+SCFramework_HCNumber = nil; // HC Number differs from ClientID; Number is an arbitrary figure
+
+// Eventhandlers/Functions
+	// Called from server-side AI spawning module that checks for ready state of a specific headless client - Randomnumber and owner ID is sent
+	// Can be used to verify said ready state for selected headless client (via ClientID) from any server-side script
+"SCFramework_PingHeadlessClient" addPublicVariableEventHandler { 
+	_Array = _this select 1;
+	_RandomNum = _Array select 0;
+	_Owner = _Array select 1;
+	if (BDC_SCFramework_DetailedLogging) then {
+		diag_log format["(SCFramework) Received ping from server (%1). Sending response back.",_RandomNum];
+	};
+	SCFramework_HCPingResponseServer = [_RandomNum,_Owner];
+	publicVariableServer "SCFramework_HCPingResponseServer";
+};
+	// One-way ping sent from server during onPlayerConnected routine that sends the headless client its unique Client ID (SCFramework_ClientID) 
+	// as well as its headless client number (1, 2, 3, etc). Can be referenced in other scripts when sending requests to server
+"SCFramework_HCSendClientID" addPublicVariableEventHandler {
+	_Array = _this select 1;
+	_SentNum = _Array select 0;
+	_SentClientID = _Array select 1;
+	_SendHCNumber = _Array select 2;
+	if (_SentNum == 1) then {
+		SCFramework_ClientID = _SentClientID; 
+		SCFramework_HCNumber = _SendHCNumber; 
+		if (BDC_SCFramework_DetailedLogging) then {
+			diag_log format["(SCFramework) Ping sent from server - Headless Client owner ID: %1 | Headless Client number: %2",SCFramework_ClientID,SCFramework_HCNumber];
+		};
+	};
+};
+	// Ping for request of FPS information from player client to server and HCs
+"SCFramework_ServerFPSRequest" addPublicVariableEventHandler {
+	_ClientID = _this select 1;
+	private["_OwnedAI","_CachedAI","_Name","_FPS","_ReturnArray","_OwnedVeh","_CachedVeh"];
+		// Gather owned AI (cached and not, alive only, no players)
+	_OwnedAI = 0;
+	_CachedAI = 0;
+	_VehArray = [];
+	{
+			// Check for AI 
+		if (local _x && alive _x && !isPlayer _x) then {
+			if (vehicle _x != _x) then {
+				if !(vehicle _x in _VehArray) then {
+					_VehArray pushBackUnique (vehicle _x) 
+				};
+			};
+			_OwnedAI = _OwnedAI + 1;
+			_isAIVCached = false;
+			_isAIVCached = _x getVariable ["isAIVCached",false]; // Fnc_AIVManager
+			if (_isAIVCached || !(simulationEnabled _x)) then {
+				_CachedAI = _CachedAI + 1;
+			};
+		};
+	} forEach allUnits;
+		// Get server FPS and FPSmin
+	_FPS = round(diag_fps);
+		// Add to array we are building to send back to client
+	_Name = "";
+	switch (SCFramework_HCNumber) do {
+		case 1 : { _Name = "HC1"; };
+		case 2 : { _Name = "HC2"; };
+		case 3 : { _Name = "HC3"; };
+		case 4 : { _Name = "HC4"; };
+		case 5 : { _Name = "HC5"; };
+	};
+	_ReturnArray = [_Name,_FPS,_OwnedAI,_CachedAI,0,0];
+	SCFramework_ServerFPSResponse = _ReturnArray;
+	_ClientID publicVariableClient "SCFramework_ServerFPSResponse";
+};
+
+// Performance/AI Ownership logging
+BDC_SCFramework_Logging = {
+	_LastLog = 0;
+	while {true} do {
+		_TimeDiff = time - _LastLog;
+		if (_TimeDiff >= BDC_SCFramework_LoggingFreq) then {
+			_LastLog = time;
+			_StartLogTime = time;
+			_FPS = diag_FPS;
+			_FPSMin = diag_fpsMin;
+			_CachedAI = 0;
+			_OwnedUnits = 0;
+			{
+				if (local _x && !isPlayer _x) then {
+					_OwnedUnits = _OwnedUnits + 1;
+					if !(simulationEnabled _x) then { 
+						_CachedAI = _CachedAI + 1; 
+					};
+				};
+			} forEach allUnits;
+			_ActiveAI = _OwnedUnits - _CachedAI;
+			diag_log format["(SCFramework) Headless Client #%5 - Current FPS: %1 | FPS Min: %2 | Locally Owned AI: %3 | Cached AI: %4 | Active AI: %6",_FPS,_FPSMin,_OwnedUnits,_CachedAI,SCFramework_HCNumber,_ActiveAI]; 
+		};
+		sleep 30;
+	};
+};
+
+// Logging routine starter
+_LogRoutine = {
+	if (BDC_SCFramework_LoggingFreq > 0) then {
+		if (BDC_SCFramework_LoggingDelay > 0) then {
+			diag_log format["(SCFramework) Delaying start of performance and AI ownership automated logging for %1 seconds.",BDC_SCFramework_LoggingDelay];
+			sleep BDC_SCFramework_LoggingDelay;
+		};
+		if (!isServer) then {
+			diag_log format["(SCFramework) Starting performance and AI ownership logging for Headless Client %1 every %2 seconds.",SCFramework_HCNumber,BDC_SCFramework_LoggingFreq];
+		} else {
+			diag_log format["(SCFramework) Starting performance and AI ownership logging for server every %2 seconds.",BDC_SCFramework_LoggingFreq];
+		};
+		[] spawn BDC_SCFramework_Logging;
+	};
+};
+
+// Request our Client ID
+if (BDC_SCFramework_DetailedLogging) then {
+	diag_log format["(SCFramework) Requesting our Client ID and Headless Client Number from server. Sending UID: %1",(getPlayerUID player)];
+	
+};
+SCFramework_HCPingRequestClientID = (getPlayerUID player);
+publicVariableServer "SCFramework_HCPingRequestClientID";
+
+// Start logging routine, if enabled
+[] spawn _LogRoutine;
+
+diag_log format["(SCFramework) HeadlessClient: Global variables reset and eventhandlers/functions loaded."];

+ 44 - 0
Scripts/scripts/SCFramework/Init_SCFramework.SQF

@@ -0,0 +1,44 @@
+/*
+	Init_SCFramework.SQF
+	
+	SCFramework initialization script that handles which instances load what processes
+*/
+
+diag_log format["(SCFramework) Loading Configuration options and functions."];
+
+#include "Config_SCFramework.SQF"; // Configuration file loaded for everyone
+Functions_SCFramework = call compile preProcessFileLineNumbers "SCFramework\Functions_SCFramework.SQF";
+
+if (!hasInterface && !isServer) then { // Dedicated headless client
+	#include "HeadlessClient_SCFramework.SQF"; 
+};
+if (isServer) then { // Server only
+	#include "Server_SCFramework.SQF";
+};
+if (hasInterface) then { // Player client only
+	#include "Player_SCFramework.SQF";
+	[] spawn {
+			// Wait until player object is created
+		waitUntil {!isNull player};
+			// GetOutMan event handler - Used for retaining vehicle ownership when player is last to exit a now otherwise empty vehicle
+		player addEventHandler ["GetOutMan", {
+			_player = _this select 0;
+			_vehicle = _this select 2;
+			_isEmpty = false;
+			if (count crew _Vehicle == 0 && alive _Vehicle) then {
+				_isEmpty = true;
+			};
+			diag_log format["(SCFramework) GetOutMan EH Tripped. Vehicle %1 %2, isEmpty %3",_Vehicle,typeOf _Vehicle,_isEmpty];
+			if (alive _vehicle && _isEmpty) then {
+				SCFramework_RequestRetainVehOwnership = [SCFramework_ClientID,_Vehicle];
+				publicVariableServer "SCFramework_RequestRetainVehOwnership";
+			};
+		}];
+			// Send ping to server requesting our Client ID
+		SCFramework_PlayerPingResponseServer = getPlayerUID player; // InitServer
+		publicVariableServer "SCFramework_PlayerPingResponseServer";
+		diag_log format["(SCFramework) Sending server request (UID %1) for my client ID.",(getPlayerUID player)];
+	};
+};
+
+diag_log format["(SCFramework) Initialization script complete."];

+ 113 - 0
Scripts/scripts/SCFramework/Player_SCFramework.SQF

@@ -0,0 +1,113 @@
+/*
+	Player_SCFramework.SQF
+	
+	Player-side global variables, functions, and eventhandlers for BDC's Client ID's Framework
+	
+	Written for A3 by ^bdc		May 2017
+*/
+
+if (!hasInterface || isServer) exitWith {}; // player clients only
+
+SCFramework_ClientID = nil; // This global variable may be referenced by any other script if needed for owner/clientID purposes
+	// Eventhandler that sent from server when initial eventhandler below is called by player client machine
+"SCFramework_PlayerSendClientID" addPublicVariableEventHandler {
+	_Array = _this select 1;
+	_SentUID = _Array select 0;
+	_SentClientID = _Array select 1;
+	if (getPlayerUID player == _SentUID) then {
+		SCFramework_ClientID = _SentClientID; // This global variable may be referenced by any other script if needed
+		diag_log format["(SCFramework) Response received from server. My Client ID: %1",SCFramework_ClientID];
+	};
+};
+	// Response from server for on-the-fly ServerFPSReport
+"SCFramework_ServerFPSResponse" addPublicVariableEventHandler {
+	if (ServerFPSReport_Enabled) then {
+		_Array = _this select 1;
+		ServerFPSReport_Received = ServerFPSReport_Received + [_Array];
+	};
+};
+
+	// Display event handler that captures keystroke to fire up ServerFPSReport
+BDC_SCFramework_ServerFPSReportKeystroke = {
+		// Passed Args
+	_dikCode = _this select 1;
+	_shiftState = _this select 2;
+	_ctrlState = _this select 3;
+	_altState = _this select 4;
+	_handled = false;
+		// Server FPS Report (Configured keystroke) (SCFramework)
+	if (_dikCode == (BDC_SCFramework_ServerFPSReport_KeyCodes select 0) && !_shiftState && !_ctrlState && !_altState) then {
+		if (ServerFPSReport_Enabled) then {
+			hintSilent "";
+			ServerFPSReport_Enabled = false;
+			cutText ["Server FPS Reporting DISABLED.", "PLAIN DOWN"];
+		} else {
+			ServerFPSReport_Enabled = true;
+			[] spawn SCFramework_DisplayFPSReport;
+			cutText ["Server FPS Reporting ENABLED.", "PLAIN DOWN"];
+		};
+		_handled = true;
+	};
+	if (_dikCode == (0x3C) && !_shiftState && !_ctrlState && !_altState) then {
+		if (BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Enable) then {
+			BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Enable = false;
+			cutText ["Automatic Transferrence of Zeus-spawned AI DISABLED.", "PLAIN DOWN"];
+		} else {
+			BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Enable = true;
+			cutText ["Automatic Transferrence of Zeus-spawned AI ENABLED.", "PLAIN DOWN"];
+			if (!BDC_SCFramework_TransferZeusOwnedAI_InitialEnable) then {
+				BDC_SCFramework_TransferZeusOwnedAI_InitialEnable = true;
+				[] spawn BDC_SCFramework_TransferZeusOwnedAI;
+			};
+		};
+	};
+	//diag_log format["keystroke EH TEST - dikcode %1 shiftstate %2 ctrlstate %3 altstate %4 handled %5",_dikCode,_shiftState,_ctrlState,_altSTate,_handled];
+	_handled
+};
+
+diag_log format["(SCFramework) Player Client: Global variables reset and eventhandlers/functions loaded."];
+
+BDC_SCFramework_TransferZeusOwnedAI = {
+	// Check if Zeus operator owns spawned-in AI
+	diag_log format["(SCFramework) Starting TransferZeusOwnedAI monitor."];
+	while {true} do {
+		_PlayerGroup = group player;
+		{
+			_Group = _x;
+			_ZeusFlagged = false;
+			_ZeusFlagged = _Group getVariable ["ZeusFlagged",false];
+			if (local _Group && (_PlayerGroup != _Group) && (!_ZeusFlagged) && BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Enable) then { // We'll skip checking the group that we're in; otherwise it'll spam the server
+				if (count units _Group > 0) then {
+					if (BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_FlagOffloadedGroups) then {
+						_Group setVariable ["ZeusFlagged",true,true]; // If enabled, only allow this group to automatically move to server once if it is moved back to a Zeus Op
+					};
+					SCFramework_ResetGroupOwnership = _Group;
+					publicVariableServer "SCFramework_ResetGroupOwnership";
+					if (BDC_SCFramework_DetailedLogging) then {
+						diag_log format["(SCFramework) Transferring locally-owned AI group %1 to server.",_Group];
+					};
+				};
+			};
+		} forEach allGroups;
+		sleep BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Frequency;
+	};
+};
+
+	// Check if admin for Server FPS report
+waitUntil {!isNull player};
+sleep 3;
+if ((getPlayerUID player) in BDC_SCFramework_ServerFPSReport_AdminUIDList) then {
+	Fnc_ServerFPSReport = compile preProcessFileLineNumbers "SCFramework\Fnc_ServerFPSReport.SQF";
+	[] spawn Fnc_ServerFPSReport;
+	player addEventHandler ["Respawn",{ [] spawn Fnc_ServerFPSReport; }];
+	BDC_SCFramework_ServerFPSReport_KeyboardPress = (findDisplay 46) displayAddEventHandler ["KeyDown","_this call BDC_SCFramework_ServerFPSReportKeystroke"];
+	diag_log format["(SCFramework) Admin eventhandlers added. Press the F1 key to access the Server FPS report."];
+};
+
+	// Automatic transfer of Zeus-owned AI
+if (BDC_SCFramework_HCOffloading_AutomaticOffLoading_TransferZeusOperatorAI_Enable) then {
+	[] spawn BDC_SCFramework_TransferZeusOwnedAI;
+	BDC_SCFramework_TransferZeusOwnedAI_InitialEnable = true;
+} else {
+	BDC_SCFramework_TransferZeusOwnedAI_InitialEnable = false;
+};

+ 520 - 0
Scripts/scripts/SCFramework/Server_SCFramework.SQF

@@ -0,0 +1,520 @@
+/*
+	Server_SCFramework.SQF
+	
+	Script file that contains all global variables, functions, and eventhandlers related to use of BDC's Server-Client Framework
+	on the server only
+	
+	Written by ^bdc		April 2017
+	Modified for automatic headless client offloading	Jan 2018
+*/
+
+// Reset global variables
+BDC_SCFramework_HeadlessClientIDs = [];
+BDC_SCFramework_HeadlessClientUIDs = [];
+BDC_SCFramework_PlayerClientIDs = [];
+BDC_SCFramework_HasDisconnectedHC = false; // This global var flags true in the event a headless client disconnects prematurely and prompts the server to re-send all HC's new headless client numbers 
+
+// Eventhandlers/Functions
+	// This eventhandler, tied with SCFramework_PingHeadlessClient, can be used as a two-way acknowledgement
+	// of the ready status of a particular headless client prior to the offloading of any AI units (setOwner/setGroupOwner)
+"SCFramework_HCPingResponseServer" addPublicVariableEventHandler {
+	_Array = _this select 1;
+	_Num = _Array select 0;
+	_Owner = _Array select 1;
+	SCFramework_PingHCResponse = [_Num,_Owner]; // This array can be referenced on a server-side spawning script to ensure headless client is connected and ready
+	if (BDC_SCFramework_DetailedLogging) then {
+		diag_log format["(SCFramework) Response received from headless client %1. Number returned: %2. Global variable array set.",_Owner,_Num];
+	};
+};
+
+	// Player client requests its client ID (uses player UID as cross-reference; table is defined and updated in onPlayerConnected)
+"SCFramework_PlayerPingResponseServer" addPublicVariableEventHandler {
+	_UID = _this select 1;
+	{
+		_Array = _x;
+		_ClientID = _Array select 0;
+		_SCUID = _Array select 1;
+		if (_SCUID == _UID) then {
+			SCFramework_PlayerSendClientID = [_UID,_ClientID];
+			_ClientID publicVariableClient "SCFramework_PlayerSendClientID"; // Player_SCFramework
+			if (BDC_SCFramework_DetailedLogging) then {
+				diag_log format["(SCFramework) Ping received from player (UID %1). Sending client ID %2 back.",_UID,_ClientID];
+			};
+		};
+	} forEach BDC_SCFramework_PlayerClientIDs; // onPlayerConnected
+};
+
+	// Headless client requests its client ID once SCFramework has completed loading on its end
+"SCFramework_HCPingRequestClientID" addPublicVariableEventHandler {
+	_UID = _this select 1;
+	_Num = 0;
+	_HCNumber = 1;
+	{
+		if (_x == _UID) then {
+			if (BDC_SCFramework_DetailedLogging) then {
+				diag_log format["(SCFramework) Client ID requested from headless client with UID %1.",_UID];
+			};
+			_ClientID = BDC_SCFramework_HeadlessClientIDs select _Num; // Select from adjacent array
+			[_ClientID,_HCNumber] spawn Fnc_SCFramework_PingHCClientID;
+		};
+		_Num = _Num + 1;
+		_HCNumber = _HCNumber + 1;
+	} forEach BDC_SCFramework_HeadlessClientUIDs;
+};
+
+	// Request for ownership transfer to specific headless client from server (if a particular routine on an HC requests it specifically)
+"SCFramework_RequestForGroupOwnership" addPublicVariableEventHandler {
+	_Array = _this select 1;
+	_Group = _Array select 0;
+	_Owner = _Array select 1;
+	_FoundHCNum = false;
+	_Counter = 1;
+	_HCNum = -1;
+	{
+		if (_x == _Owner && !_FoundHCNum) then {
+			_FoundHCNum = true;
+			_HCNum = _Counter;
+		};
+		_Counter = _Counter + 1;
+	} forEach BDC_SCFramework_HeadlessClientIDs; // onPlayerConnected
+	if (BDC_SCFramework_DetailedLogging) then {
+		diag_log format["(SCFramework) Request from headless client #%1 (Client ID %2) for transference of ownership of group %3",_HCNum,_Owner,_Group];
+	};
+	_Return = _Group setGroupOwner _Owner;
+	_Return
+};
+
+	// This event handler is sent from a player client automatically when a client exits the vehicle from the driver/pilot seat 
+	// so ownership of the vehicle is retained by that player instead of being automatically xferred back to the server
+	// Also works with grabbing ownership of a vehicle while sling loading - called from Player_SCFramework from GetOutMan EH
+"SCFramework_RequestRetainVehOwnership" addPublicVariableEventHandler {
+	_Array = _this select 1;
+	_ClientID = _Array select 0;
+	_Vehicle = _Array select 1;
+	_Driver = driver _Vehicle;
+		// Only allow if vehicle is still alive and no driver is in it
+	if (BDC_SCFramework_ClientRetainVehOwnership && (_Vehicle isKindOf "LandVehicle" || _Vehicle isKindOf "Ship" || _Vehicle isKindOf "Air")) then {
+		if (BDC_SCFramework_DetailedLogging) then {
+			diag_log format["(SCFramework) Client ID %1 requesting to retain ownership of vehicle %2 %3",_ClientID,_Vehicle,(typeOf _Vehicle)];
+		};
+		if (alive _Vehicle && (isNull _Driver)) then {
+			if (BDC_SCFramework_DetailedLogging) then {
+				diag_log format["(SCFramework) Setting ownership of vehicle %1 %2 to Client ID %3.",_Vehicle,(typeOf _Vehicle),_ClientID];
+			};
+			_Vehicle setOwner _ClientID;
+		};
+	};
+};
+
+	// Ping for request of FPS information from player client to server and HCs
+"SCFramework_ServerFPSRequest" addPublicVariableEventHandler {
+	_ClientID = _this select 1;
+	private["_OwnedAI","_CachedAI","_Name","_FPS","_ReturnArray","_OwnedVeh","_CachedVeh"];
+		// Gather owned AI (cached and not, alive only, no players)
+	_OwnedAI = 0;
+	_CachedAI = 0;
+	{
+			// Check for AI 
+		if (local _x && alive _x && !isPlayer _x) then {
+			_OwnedAI = _OwnedAI + 1;
+			_isAIVCached = false;
+			_isAIVCached = _x getVariable ["isAIVCached",false]; // Fnc_AIVManager
+			if (_isAIVCached || !(simulationEnabled _x)) then {
+				_CachedAI = _CachedAI + 1;
+			};
+		};
+	} forEach allUnits;
+	_OwnedVeh = 0;
+	_CachedVeh = 0;
+	{
+		if (alive _x && !isPlayer _x) then {
+			if (_x isKindOf "LandVehicle" || _x isKindOf "Ship" || _x isKindOf "Air") then {
+				_OwnedVeh = _OwnedVeh + 1;
+				_isAIVCached = false;
+				_isAIVCached = _x getVariable ["isAIVCached",false]; // Fnc_AIVManager
+				if (_isAIVCached || !(simulationEnabled _x)) then {
+					_CachedVeh = _CachedVeh + 1;
+				};
+			};
+		};
+	} forEach vehicles;
+		// Get server FPS and FPSmin
+	_FPS = round(diag_fps);
+	_ReturnArray = ["Server",_FPS,_OwnedAI,_CachedAI,_OwnedVeh,_CachedVeh];
+	SCFramework_ServerFPSResponse = _ReturnArray;
+	_ClientID publicVariableClient "SCFramework_ServerFPSResponse";
+};
+	// Reset group ownership back to server
+"SCFramework_ResetGroupOwnership" addPublicVariableEventHandler {
+	_Group = _this select 1;
+	_Group setGroupOwner 2;
+	if (BDC_SCFramework_DetailedLogging) then {
+		diag_log format["(SCFramework) SCFramework_ResetGroupOwnership EH called. Resetting ownership of group %1 to server.",_Group];
+	};
+};
+
+	// Function written to gather the owner/clientID of a specific headless client based on login order (1, 2, 3, etc)
+	// Used by server-side mods that are configured to offload AI to a specific headless client by number instead of by name
+	// How to call:
+	// _HCClientID = [(Number of headless client logged in)] call Fnc_SCFramework_GetHCClientID;
+	// _HCClientID = [(Number of hc logged in),"AI Spawner Script Name"] call Fnc_SCFramework_GetHCClientID;
+Fnc_SCFramework_GetHCClientID = {
+	_RequestNum = _this select 0;
+	_ModuleName = "";
+	if (count _this > 1) then {
+		_ModuleName = _this select 1;
+	};
+	_ReturnID = -1; // default
+	if (count BDC_SCFramework_HeadlessClientIDs > 0) then {
+		_Num = _RequestNum - 1; // onPlayerConnected
+		_ReturnID = BDC_SCFramework_HeadlessClientIDs select _Num;
+		if (_ModuleName != "") then {
+			if (BDC_SCFramework_DetailedLogging) then {
+				diag_log format["(SCFramework) Request from module %1 for owner/ClientID of headless client #%2 - Returning ClientID %3",_ModuleName,_RequestNum,_ReturnID];
+			};
+		} else {
+			if (BDC_SCFramework_DetailedLogging) then {
+				diag_log format["(SCFramework) Request for owner/ClientID of headless client #%1 - Returning ClientID %2",_RequestNum,_ReturnID];
+			};
+		};
+	} else {
+		if (BDC_SCFramework_DetailedLogging) then {
+			diag_log format["(SCFramework) Function called but currently no headless clients logged in to return a client ID. Returning -1."];
+		};
+	};
+	_ReturnID
+};
+
+	// Function that will retrieve the ClientID by player UID
+	// How to call from server:
+	// _PlayerClientID = ["Player UID string format"] call Fnc_SCFramework_GetPlayerClientID;
+Fnc_SCFramework_GetPlayerClientID = {
+	_UID = _this select 0;
+	_ReturnID = -1; // Default
+	if (count BDC_SCFramework_PlayerClientIDs > 0) then { // onPlayerConnected
+		{
+			_CUID = _x select 1;
+			if (_UID == _CUID) then {
+				_ReturnID = _x select 0;
+			};
+		} forEach BDC_SCFramework_PlayerClientIDs;
+	} else {
+		_ReturnID = -1;
+	};
+	if (BDC_SCFramework_DetailedLogging) then {
+		diag_log format["(SCFramework) Owner of player UID %1 requested. ClientID returned: %1",_ReturnID];
+	};
+	_ReturnID
+};
+
+	// Set group ownership function (called from server to move a group to a specific clientID/owner)
+	// How to call from a server-side script:
+	// [(Group object),(Destination Client ID),"AI Spawning Script Name"] call Fnc_SCFramework_SetGroupOwner;
+Fnc_SCFramework_SetGroupOwner = {
+	_Group = _this select 0;
+	_ClientID = _this select 1;
+	_ModuleName = _this select 2; // Module/Script name for logging purposes; ex. "ExileZ","DMS", etc - May also leave blank in quotes if not using SpecificAITable offloading
+	if (_ModuleName == "") then {
+		if (BDC_SCFramework_DetailedLogging) then {
+			diag_log format["(SCFramework) Request from server to send group %1 to Client ID %2.",_Group,_ClientID];
+		};
+	} else {
+		if (BDC_SCFramework_DetailedLogging) then {
+			diag_log format["(SCFramework) Request from server module %1 to send group %2 to Client ID %3.",_ModuleName,_Group,_ClientID];
+		};
+	};
+		// Move the group and all AI units
+	_Return = _Group setGroupOwner _ClientID;
+	if (BDC_SCFramework_DetailedLogging) then {
+		diag_log format["(SCFramework) Group %1 Module %2 Destination ClientID %3 SetGroupOwner Request response: %4",_Group,_ModuleName,_ClientID,_Return];
+	};
+	_Return
+};
+
+	// Sub-function called by _HCOffloading in gathering number of AI owned by each headless client - Returns array
+Fnc_SCFramework_BuildHCAIArray = {
+	// Build our list of AI's owned by each headless client
+	_HCAIArray = []; // Nested arrays: [[(HCOwnernNumber),(NumberOfAIOwned)]
+	{
+		_OwnedAI = 0; // default
+		_HCOwner = _x;
+		_BuildArray = [];
+		{
+			if (owner _x == _HCOwner) then {
+				_OwnedAI = _OwnedAI + 1;
+			};
+		} forEach allUnits;
+		_BuildArray = [_HCOwner,_OwnedAI];
+		_HCAIArray pushBackUnique _BuildArray;
+	} forEach BDC_SCFramework_HeadlessClientIDs;
+	_HCAIArray
+};
+
+	// Automatic Headless Client offloading sub-function
+Fnc_SCFramework_HCOffloading = {
+		// Start delay
+	if (BDC_SCFramework_HCOffloading_StartDelay > 0) then {
+		if (BDC_SCFramework_DetailedLogging) then {
+			diag_log format["(SCFramework) Start delay configured for Headless Client offloading of %1 seconds...",BDC_SCFramework_HCOffloading_StartDelay];
+		};
+		sleep BDC_SCFramework_HCOffloading_StartDelay;
+	};
+		// Grab HC client ID's
+	_HeadlessClientIDs = [];
+	if (count BDC_SCFramework_HeadlessClientIDs == 0) exitWith {
+		diag_log format["(SCFramework) No headless clients connected to the server. HC Offloading suspended."];
+	};
+		// Start function
+	diag_log format["(SCFramework) Starting specific headless client offloading."];
+	while {true} do {
+			// Check all existing groups to see if we can offload any	
+		_AllGroups = allGroups;
+		_GroupsToOffload = [];
+			// Check spawntime on each group first
+		{
+					// Check creation time of group - We use this to keep track of group lifetime prior to automatic offloading
+			_Group = _x;
+			_SpawnTime = 0;
+			_SpawnTime = _Group getVariable ["SpawnTime",0];
+			if (_SpawnTime == 0) then {
+				_Group setVariable ["SpawnTime",time,true];
+			};
+		} forEach _AllGroups;
+			// Specific AI Table - For groups of AI that've been spawned in using Fnc_SCFramework_CreateGroup via script and configured in SpecificAITable to be offloaded to a specific headless client
+		if (BDC_SCFramework_HCOffloading_SpecificAITable_Enable) then {
+			{
+				_Array = _x;
+				_ModuleName = _Array select 0;
+				_VariableName = _Array select 1;
+				_HCClientNum = _Array select 2;
+				if (_HCClientNum != 0) then { // If set to 0, then that's server; ignore it
+					{
+						_Group = _x;
+						if (local _Group) then {
+							_Var = false;
+							_Var = _Group getVariable [_VariableName,false];
+							_SpawnTime = time;
+							_SpawnTime = _Group getVariable ["SpawnTime",0];
+							_TimeDiff = time - _SpawnTime;
+							if (_Var && (_TimeDiff >= BDC_SCFramework_HCOffloading_GroupTimerMinimum)) then {
+								_AllGroups = _AllGroups - [_Group];
+									// Grab specific HC Client ID from client number passed
+								_Num = _HCClientNum - 1;
+								_HCClientID = BDC_SCFramework_HeadlessClientIDs select _Num;
+									// Add record to array
+								_AddToOffloadArray = [];
+								_AddToOffloadArray = [_Group,_HCClientID,_ModuleName];
+								_GroupsToOffload pushbackUnique _AddToOffloadArray;
+							};
+						};
+					} forEach _AllGroups;
+				};
+			} forEach BDC_SCFramework_HCOffloading_SpecificAITable;
+		};
+			// Did we collect any specific-configured groups to offload? Let's go
+		if (count _GroupsToOffload > 0) then {
+			{
+				_Array = _x;
+				_Group = _Array select 0;
+				_HCClientID = _Array select 1;
+				_ModuleName = _Array select 2;
+				[_Group,_HCClientID,_ModuleName] call Fnc_SCFramework_SetGroupOwner;
+				sleep 0.25; // Brief delay in between each
+			} forEach _GroupsToOffload;
+		};
+			// Automatic offloading, if configured
+		if (BDC_SCFramework_HCOffloading_AutomaticOffloading_Enable && (count _AllGroups > 0)) then {	
+				// We're using _allGroups as it's already had groups removed per specific AI table config prior to this - So we'll look for groups that we can automatically offload that are leftover
+				// Gather ownership numbers from each HC, if any connected
+			if (count BDC_SCFramework_HeadlessClientIDs > 0) then {
+				_HCAIArray = [] call Fnc_SCFramework_BuildHCAIArray; // Build our array of live AI owned by each headless client
+				{
+					_Group = _x;
+					if (local _Group) then {
+						_SpawnTime = time;
+						_SpawnTime = _Group getVariable ["SpawnTime",0];
+						_TimeDiff = time - _SpawnTime;
+						if (_TimeDiff >= BDC_SCFramework_HCOffloading_GroupTimerMinimum) then { // Group that server owns that's been around long enough to offload - let's go
+							_FoundHC = false;
+							//_HCAIArray = [] call Fnc_SCFramework_BuildHCAIArray; // Build our array of live AI owned by each headless client
+							_HCCount = 0;
+							{
+								_BuildArray = _x;
+								_HCOwner = _BuildArray select 0;
+								_OwnedAI = _BuildArray select 1;
+								if (!_FoundHC && (_OwnedAI < BDC_SCFramework_HCOffloading_AutomaticOffloading_MaxAIPerHeadlessClient)) then {
+									_FoundHC = true;
+									_GroupCount = count (units _Group);
+									_NewOwnedAI = _OwnedAI + _GroupCount;
+									_BuildArray = [_HCOwner,_NewOwnedAI];
+									_HCAIArray set [_HCCount,_BuildArray];
+									[_Group,_HCOwner,""] call Fnc_SCFramework_SetGroupOwner;
+									sleep 0.25; // Brief delay in between each
+								};
+								_HCCount = _HCCount + 1;
+							} forEach _HCAIArray;
+						};
+					};
+				} forEach _allGroups;
+			};
+		};
+		sleep BDC_SCFramework_HCOffloading_Frequency;
+	};
+};
+
+// Headless Client automated AI offloading
+if (BDC_SCFramework_HCOffloading_Enable) then {
+	[] spawn Fnc_SCFramework_HCOffloading;
+};
+
+// Performance/AI Ownership logging
+BDC_SCFramework_Logging = {
+	_LastLog = 0;
+	while {true} do {
+		_TimeDiff = time - _LastLog;
+		if (_TimeDiff >= BDC_SCFramework_LoggingFreq) then {
+			_LastLog = time;
+			_StartLogTime = time;
+			_FPS = diag_FPS;
+			_FPSMin = diag_fpsMin;
+			_CachedAI = 0;
+			_OwnedUnits = 0;
+			{
+				if (local _x && !isPlayer _x) then {
+					_OwnedUnits = _OwnedUnits + 1;
+					if !(simulationEnabled _x) then { 
+						_CachedAI = _CachedAI + 1; 
+					};
+				};
+			} forEach allUnits;
+			_ActiveAI = _OwnedUnits - _CachedAI;
+			if (!isServer) then {
+				diag_log format["(SCFramework) Headless Client #%5 - Current FPS: %1 | FPS Min: %2 | Locally Owned AI: %3 | Cached AI: %4 | Active AI: %6",_FPS,_FPSMin,_OwnedUnits,_CachedAI,SCFramework_HCNumber,_ActiveAI]; 
+			} else {
+				diag_log format["(SCFramework) Server - Current FPS: %1 | FPS Min: %2 | Locally Owned AI: %3 | Cached AI: %4 | Active AI: %5",_FPS,_FPSMin,_OwnedUnits,_CachedAI,_ActiveAI];
+				diag_log format["(SCFramework) Connected Headless Clients: %1",(count BDC_SCFramework_HeadlessClientIDs)];
+			};
+		};
+		sleep 30;
+	};
+};
+
+// Ping headless client to send it its client ID after connection (called from onPlayerConnected below)
+Fnc_SCFramework_PingHCClientID = {
+	_owner = _this select 0;
+	_count = _this select 1;
+	if (BDC_SCFramework_DetailedLogging) then {
+		diag_log format["(SCFramework) Sending ping to headless client owner %1 with HCNumber %2.",_owner,_count];
+	};
+	SCFramework_HCSendClientID = [1,_owner,_count];
+	_owner publicVariableClient "SCFramework_HCSendClientID";
+};
+
+// OnPlayerConnected (or headless client) - used to dole out client IDs
+Fnc_SCFramework_onPlayerConnected = {
+		// Passed Args
+	_UID = _this select 0;
+	_name = _this select 1;
+	_owner = _this select 2;
+		// Determine if headless client logging in
+	_isHC = false;
+	_NewStr = _UID select [0,2]; // Grab first 2 letters of the UID; all HC's logging in use 'HC' and then a number (drawn from PID of arma.exe process in TaskMan)
+	if (_NewStr == "HC") then { 
+		_isHC = true; 
+		BDC_SCFramework_HeadlessClientIDs pushBackUnique _owner;
+		BDC_SCFramework_HeadlessClientUIDs pushBackUnique _UID; // This array is used to cross-reference ping to client ID when HC initially pings server requesting its client ID - ^bdc
+		diag_log format["(SCFramework) onPlayerConnected: Headless Client %1 %2 with ClientID %3 connected.",_UID,_name,_owner];
+		diag_log format["(SCFramework) Current number of headless clients now connected: %1",(count BDC_SCFramework_HeadlessClientIDs)];
+		if (!BDC_SCFramework_HasDisconnectedHC) then {
+			[_owner,(count BDC_SCFramework_HeadlessClientIDs)] spawn Fnc_SCFramework_PingHCClientID; 
+		} else {
+			BDC_SCFramework_HasDisconnectedHC = false;
+			diag_log format["(SCFramework) Crashed/Disconnected Headless Client re-connecting. Re-sending ID's to all HC's."];
+			_Count = 1;
+			{
+				[_x,_Count] spawn Fnc_SCFramework_PingHCClientID;
+				_Count = _Count + 1;
+			} forEach BDC_SCFramework_HeadlessClientIDs;
+		};
+	} else { // Keep track of player client id's in a separate array
+		if (_name != "__SERVER__") then {
+			if (_UID in BDC_SCFramework_ServerFPSReport_AdminUIDList) then {
+				diag_log format["(SCFramework) onPlayerConnected: ADMIN %1 (%2) has connected.",_name,_UID];
+			} else {
+				diag_log format["(SCFramework) onPlayerConnected: Player %1 (%2) has connected.",_name,_UID];
+			};
+			_SCArray = [_owner,_UID]; // Two element array containing client ID (from owner) and the player UID (so as to remove possibility of difference with player names)
+			BDC_SCFramework_PlayerClientIDs pushBackUnique _SCArray;
+		};
+	};
+};
+
+// OnPlayerDisconnected (or HC) - We are using this only to track the disconnection of a headless client
+Fnc_SCFramework_onPlayerDisconnected = {
+		// Passed Args
+	_UID = _this select 0;
+	_name = _this select 1;
+	_owner = _this select 2;
+		// Determine if headless client logging in
+	_isHC = false;
+	_NewStr = _UID select [0,2]; 
+	if (_NewStr == "HC") then { 
+		BDC_SCFramework_HeadlessClientIDs = [BDC_SCFramework_HeadlessClientIDs,_owner] call Fnc_SCFramework_DeleteArrayElement; // Functions_SCFramework
+		BDC_SCFramework_HeadlessClientUIDs = [BDC_SCFramework_HeadlessClientUIDs,_UID] call Fnc_SCFramework_DeleteArrayElement;
+		diag_log format["(SCFramework) Headless Client %1 %2 with ClientID %3 has either crashed or disconnected in an unknown manner. Removing from global arrays.",_UID,_name,_owner];
+		diag_log format["(SCFramework) Current number of headless clients now connected: %1",(count BDC_SCFramework_HeadlessClientIDs)];
+		BDC_SCFramework_HasDisconnectedHC = true; // Will flag false if another HC logs in; reason being is we want to re-shuffle all the headless client ID's after one crashes and re-connects successfully
+	} else {
+		diag_log format["(SCFramework) Player %1 (%2) disconnected.",_name,_UID];
+		_TArray = [_owner,_UID];
+		BDC_SCFramework_PlayerClientIDs = BDC_SCFramework_PlayerClientIDs - [_TArray];
+	};
+};
+
+
+// Group manager (empty group deletion) function
+Fnc_SCFramework_GroupManager = {
+	sleep BDC_SCFramework_HCOffloading_StartDelay; // Sleep before starting - we wait until the HCOffloading start delay has expired
+	diag_log format["(SCFramework) Server - Starting empty group deletion manager."];
+	while {true} do {
+		_Ctr = 0;
+		_CountBefore = count allGroups;
+		{
+			if (count units _x == 0) then {
+				deleteGroup _x; // Delete empty group
+			};
+		} forEach allGroups;
+		_CountAfter = count allGroups;
+		_Ctr = (_CountBefore - _CountAfter);
+		if (_Ctr > 0) then {
+			diag_log format["(SCFramework) Group Manager: Deleted %1 empty groups. Groups prior: %2 | Active groups remaining: %3",_Ctr,_CountBefore,_CountAfter];
+		};
+		sleep 300; // Run once every 5 minutes
+	};
+};
+
+	// Performance logging routine
+_LogRoutine = {
+	if (BDC_SCFramework_LoggingFreq > 0) then {
+		if (BDC_SCFramework_LoggingDelay > 0) then {
+			diag_log format["(SCFramework) Delaying start of performance and AI ownership automated logging for %1 seconds.",BDC_SCFramework_LoggingDelay];
+			sleep BDC_SCFramework_LoggingDelay;
+		};
+		diag_log format["(SCFramework) Starting performance and AI ownership logging for server every %1 seconds.",BDC_SCFramework_LoggingFreq];
+		[] spawn BDC_SCFramework_Logging;
+	};
+};
+
+// Stacked EH for onPlayerConnected and onPlayerDisconnected - track when clients connect and disconnect/crash
+["SCFrameworkOnPlayerConnected", "onPlayerConnected", { [_uid, _name, _owner] call Fnc_SCFramework_onPlayerConnected; }] call BIS_fnc_addStackedEventHandler;
+["SCFrameworkOnPlayerDisconnected", "onPlayerDisconnected", { [_uid, _name, _owner] call Fnc_SCFramework_onPlayerDisconnected; }] call BIS_fnc_addStackedEventHandler;	
+
+// Start logging routine, if enabled
+[] spawn _LogRoutine;
+
+// Start group manager
+if (BDC_SCFramework_GroupManagerEnable) then {
+	[] spawn Fnc_SCFramework_GroupManager;
+};
+
+diag_log format["(SCFramework) Server: Headless and player client ID's arrays initialized. SCFramework server-side eventhandlers and functions loaded."];

+ 6 - 1
Util/Mission Merger/mission_merger.py

@@ -1,10 +1,15 @@
 import os
+from distutils.dir_util import *
 dir = os.path.dirname(__file__)
 filename = os.path.join(dir, '../../Missions/')
 
 print(filename)
+tmp_file = filename+"Temp/"
 
 files = os.listdir(filename)
 for name in files:
     if name.endswith('.txt') == False:
-        print(name)
+        print(name)
+        src_dir = filename+name
+        tmp_dir = tmp_file+name
+        copy_tree(src_dir, tmp_dir)