--[[
			Advanced Tools for Mission Editor - ATME
			Author : Sunski34
			
			Testers : Snowsniper/sunski34

			Oriented Object lua core module
			
			V.1.0.0 - Beta Version - 10/2016
			
			DO NOT MODIFY ANYTHING - For any idea or bug, please send a message on the forum
			
			Notes :
			-----
			ATME function initObjectsInMission is based on existing MIST functions
			
			All comments in that file are in french because of ATME_Core software debugging. If you want 
			informations about ATME, classes, functions, events or handlers, please refer to english documentation.
]]--


----------------------------------------------------------------------------------------------------------
-- DEFINITION DES VARIABLES GLOBALES
----------------------------------------------------------------------------------------------------------

local majorVersion = 1
local minorVersion = 0

local allGroupsInMission = {}			-- Fixe juste les objets définis dans la mission
local allAIUnitsInMission = {}			-- Fixe juste les objets définis dans la mission
local allStaticObjectsInMission = {}	-- Fixe juste les objets définis dans la mission
local allPlayerUnitsInMission = {}		-- Fixe juste les objets définis dans la mission
local allStaticAirbasesInMission = {}	-- Fixe et pas de spawn sur les airfiels et FARP

local allAIUnitsAndPlayers = {}			-- Liste de toutes les unités actives joueurs et AI.
local signalCapabilityUnits = {}		-- Liste des unités actives pouvant déclencher un signal

local players = {}						-- Players actifs
local oldNbPlayers = 0

local groups = {}						-- Groupes actifs
local AIUnits = {}						-- AIUnit actifs
local totalAIUnits = 1					-- Nombre total + 1 d'unités créées à un instant t (utile au spawn)
local staticObjects = {}				-- Objets statiques actifs
local races = {}						-- Courses créées

local airbasesByID = {}					-- Liste des airbases du théatre (fixes pour un théatre donné)
local helipadsAirFieldsByID = {}		-- Liste des airfields et FARP créées dans la mission

local smokeColors = {
	["GREEN"] = trigger.smokeColor.Green,
	["RED"] = trigger.smokeColor.Red,
	["WHITE"] = trigger.smokeColor.White,
	["ORANGE"] = trigger.smokeColor.Orange,
	["BLUE"] = trigger.smokeColor.Blue,
	["RANDOM"] = -1,
}

local flareColors = {
	["GREEN"] = trigger.flareColor.Green,
	["RED"] = trigger.flareColor.Red,
	["WHITE"] = trigger.flareColor.White,
	["YELLOW"] = trigger.flareColor.Yellow,
	["RANDOM"] = -1,
}

local flareColorsIndex = {
	"GREEN",
	"RED",
	"WHITE",
	"YELLOW"
}
local smokeColorsIndex = {
	"GREEN",
	"RED",
	"WHITE",
	"ORANGE",
	"BLUE",
}

local groupCategories = {
	["plane"] = Group.Category.AIRPLANE,
	["helicopter"] = Group.Category.HELICOPTER,
	["vehicle"] = Group.Category.GROUND,
	["ship"] = Group.Category.SHIP,
}

local categoryNames = {
	[Group.Category.AIRPLANE] = "AIRPLANE",
	[Group.Category.HELICOPTER] = "HELICOPTER",
	[Group.Category.GROUND] = "GROUND",
	[Group.Category.SHIP] = "SHIP",
}

local coalitionNames = {
	[coalition.side.NEUTRAL] = "NEUTRAL",
	[coalition.side.RED] = "RED",
	[coalition.side.BLUE] = "BLUE",
}

local coalitionIds = {
	["NEUTRAL"] = coalition.side.NEUTRAL,
	["RED"] = coalition.side.RED,
	["BLUE"] = coalition.side.BLUE,
}

local countries = {
	["RUSSIA"] = 0,
	["UKRAINE"] = 1,
	["USA"] = 2,
	["TURKEY"] = 3,
	["UK"] = 4,
	["FRANCE"] = 5,
	["GERMANY"] = 6,
	["AGGRESSORS"] = 7,
	["CANADA"] = 8,
	["SPAIN"] = 9,
	["THE_NETHERLANDS"] = 10,
	["BELGIUM"] = 11,
	["NORWAY"] = 12,
	["DENMARK"] = 13,
	["ISRAEL"] = 15,
	["GEORGIA"] = 16,
	["INSURGENTS"] = 17,
	["ABKHAZIA"] = 18,
	["SOUTH_OSETIA"] = 19,
	["ITALY"] = 20,
	["AUSTRALIA"] = 21,
	["SWITZERLAND"] = 22,
	["AUSTRIA"] = 23,
	["BELARUS"] = 24,
	["BULGARIA"] = 25,
	["CHEZH_REPUBLIC"] = 26,
	["CHINA"] = 27,
	["CROATIA"] = 28,
	["EGYPT"] = 29,
	["FINLAND"] = 30,
	["GREECE"] = 31,
	["HUNGARY"] = 32,
	["INDIA"] = 33,
	["IRAN"] = 34,
	["IRAQ"] = 35,
	["JAPAN"] = 36,
	["KAZAKHSTAN"] = 37,
	["NORTH_KOREA"] = 38,
	["PAKISTAN"] = 39,
	["POLAND"] = 40,
	["ROMANIA"] = 41,
	["SAUDI_ARABIA"] = 42,
	["SERBIA"] = 43,
	["SLOVAKIA"] = 44,
	["SOUTH_KOREA"] = 45,
	["SWEDEN"] = 46,
	["SYRIA"] = 47,
}

local personnelCarrierAttributes = {
	["UH-1H"] = {maxUnits = 8, minDistance = 15,},
	["Mi-8MT"] = {maxUnits = 16, minDistance = 18,},
	["SA342M"] = {maxUnits = 2, minDistance = 12,},
	["SA342L"] = {maxUnits = 2, minDistance = 12,},
	["SA342Mistral"] = {maxUnits = 2, minDistance = 12,},
	["AAV7"] = {maxUnits = 25, minDistance = 10,},
	["M-113"] = {maxUnits = 11, minDistance = 10,},
	["LAV-25"] = {maxUnits = 6, minDistance = 10,},
	["M1126 Stryker ICV"] = {maxUnits = 9, minDistance = 10,},
	["M-2 Bradley"] = {maxUnits = 6, minDistance = 10,},
	["BTR-80"] = {maxUnits = 9, minDistance = 10,},
	["BTR_D"] = {maxUnits = 12, minDistance = 10,},
	["MTLB"] = {maxUnits = 10, minDistance = 10,},
	["BMD-1"] = {maxUnits = 4, minDistance = 10,},
	["BMP-1"] = {maxUnits = 8, minDistance = 10,},
	["BMP-2"] = {maxUnits = 7, minDistance = 10,},
	["BMP-3"] = {maxUnits = 7, minDistance = 10,},
	["UAZ-469"] = {maxUnits = 6, minDistance = 10,},
	["Tigr_233036"] = {maxUnits = 6, minDistance = 10,},
	["GAZ-3307"] = {maxUnits = 16, minDistance = 10,},
	["GAZ-3308"] = {maxUnits = 16, minDistance = 10,},
	["Ural-4320T"] = {maxUnits = 20, minDistance = 10,},
	["Ural-4320-31"] = {maxUnits = 20, minDistance = 10,},
	["M 818"] = {maxUnits = 20, minDistance = 10,},
	["KAMAZ Truck"] = {maxUnits = 20, minDistance = 10,},
}

local cargos = {
	["BASIC"] = {
		type = "uh1h_cargo",
		shape = "ab-212_cargo",
	},
	["TRUNKS SMALL"] = {
		type = "trunks_small_cargo",
		shape = "trunks_small_cargo",
	},
	["TRUNKS LONG"] = {
		type = "trunks_long_cargo",
		shape = "trunks_long_cargo",
	},
	["TETRAPOD"] = {
		type = "tetrapod_cargo",
		shape = "tetrapod_cargo",
	},
	["PIPE SMALL"] = {
		type = "pipes_small_cargo",
		shape = "pipes_small_cargo",
	},
	["PIPE BIG"] = {
		type = "pipes_big_cargo",
		shape = "pipes_big_cargo",
	},
	["OIL TANK"] = {
		type = "oiltank_cargo",
		shape = "oiltank_cargo",
	},
	["M117"] = {
		type = "m117_cargo",
		shape = "m117_cargo",
	},
	["SMALL CONTAINER"] = {
		type = "iso_container_small_cargo",
		shape = "iso_container_small",
		rate = 220,
	},
	["F BAR"] = {
		type = "f_bar_cargo",
		shape = "f_bar_cargo",
		rate = 823,
	},
	["FUEL TANK"] = {
		type = "fueltank_cargo",
		shape = "fueltank_cargo",
	},
	["BW CONTAINER"] = {
		type = "container_cargo",
		shape = "bw_container_cargo",
	},
	["CONTAINER"] = {
		type = "iso_container_cargo",
		shape = "iso_container",
		rate = 3800,
	},
	["BARRELS"] = {
		type = "barrels_cargo",
		shape = "barrels_cargo",
	},
	["AMMO BOX"] = {
		type = "ammo_cargo",
		shape = "ammo_box_cargo",
	},
}

local supportedLanguages = {
	["FR"] = "French",
	["EN"] = "English",
	["RUS"] = "Russian",
	["GER"] = "German",
}

local exitATME = false					-- Flag de sortie d'ATME, en cas d'erreur. cf errorOutputCore
local ATMEDebugSwitch = false			-- Activation ou non du mode affichage debug pour le module Core

local language = "FR"					-- Langue utilisée par défaut
local isRunning = false					-- isRunning est à false jusqu'au lancement par ATME.run après chargement des modules
local theatreName = ""
local maxTimeSmoke = 300
local debugLevel = 0					-- Niveau de debug
local maxGroupId = 0					-- Récupère le plus grand groupId de la mission... Nécessaire au duplicateFromMissionDatas.

local activatedRadioTransmissions 		-- Prédéclaration, c'est un ATME.C_IndexList déclaré plus loin

local POINT_BAD = 0
local POINT_2D = 1
local POINT_3D = 2

local customEventHandler = {}			-- Déclaration du handler d'événements DCS

local readyToBoardGroups = {}			-- Liste des groupes prêts à embarquer
local groupsOnBoard = {}				-- Liste des groupes embarqués, en cas de destruction du transport
local pickupZones = {}					-- Liste des zones DCS déclarées pickupZones.
local newStaticObjectsCategory = {}		-- Liste des objets statiques spawnés et non encore créés suite à S_EVENT_BIRTH

----------------------------------------------------------------------------------------------------------
-- DEFINITION DES FONCTIONS UTILES DIRECTEMENT A ATME
----------------------------------------------------------------------------------------------------------

local function errorOutputCore(_module, _text)
	-- errorOutputCore : fonction de base de gestion des erreurs, engendre un arrêt de ATME (plus de réarmement des timers
	
	if (ATME.isObjectClass(_module, ATME.C_Module) == true or _module == nil) and type(_text) == "string" then
		local label = "[ATME"
		if _module ~= nil then
			label = label .. "-" .. _module:getName()
		end
		label = label .. "] " .. _text .. "\nATME Stopped"
		env.error(label, true)
	else
		env.error("[ATME] Function errorOutputCore : Bad parameters\nATME Stopped", true)
	end
	
	exitATME = true
end

local function outputCore(_module, _text, _level)
	-- outputCore : fonction de base des messages de debug en fonction du niveau et de l'activation ou non du mode debug
	
	if (ATME.isObjectClass(_module, ATME.C_Module) == true or _module == nil) and type(_text) == "string" and type(_level) == "number" then
		local label = "[ATME"
		local debugMode = ATMEDebugSwitch
		
		if _module ~= nil then
			label = label .. "-" .. _module:getName()
			debugMode = _module:isDebugOn()
		end
		
		label = label .. "] " .. _text
		if _level > 0 and _level <= debugLevel and debugMode == true then
			trigger.action.outText(label, 5)
		end
		
		env.info(label, false)
	else
		if ATME.isObjectClass(_module, ATME.C_Module) == false and  _module ~= nil then
			errorOutputCore(nil, "Function outputCore : module type must be nil or ATME.C_Module")
		elseif type(_text) ~= "string" then
			errorOutputCore(nil, "Function outputCore : text type must be string")
		elseif type(_level) ~= "number" then
			errorOutputCore(nil, "Function outputCore : level type must be number")
		end
	end
end

local function initRadioTransmission(_file, _point, _modulation, _frequency, _power, _unitName)
	-- initRadioTransmission : fonction d'initialisation des paramètres pour les transmissions radio
	-- 						   pour une mise en forme au format DCS
	
	local newRadioTransmission = {}
	
	newRadioTransmission.file = _file
	newRadioTransmission.point = ATME.convertToPoint3D(_point)
	
	if _modulation == "FM" then 
		newRadioTransmission.modulation = radio.modulation.FM
	else
		newRadioTransmission.modulation = radio.modulation.AM
	end
	
	newRadioTransmission.frequency = _frequency
	newRadioTransmission.power = _power

	newRadioTransmission.entry = activatedRadioTransmissions:add(newRadioTransmission)
	newRadioTransmission.textId = "RT_ATME_" .. newRadioTransmission.entry
	newRadioTransmission.unitName = _unitName
	
	return newRadioTransmission
end

local function startDCSRadioTransmission(_file, _point, _modulation, _repeat, _frequency, _power, _textId)
	-- Pour "trigger.action.radioTransmission", si 4è paramètre à true, la boucle est gérée par DCS directement.
	
	trigger.action.radioTransmission("l10n/DEFAULT/" .. _file, _point , _modulation, _repeat, _frequency, _power, _textId)
end

local function stopDCSRadioTransmission(_textId)
	-- Pour "trigger.action.radioTransmission", si 4è paramètre à true, la boucle est gérée par DCS directement.
	
	trigger.action.stopRadioTransmission(_textId)
end								

local function crossAxis(_A, _B, _C, _oldSide, _direction)
	local side = ATME.whichSide(_A, _B, _C)
	local ok = false
	local oldSide = _oldSide
	
	if oldSide ~= nil and oldSide ~= side and side == _direction then
		ok = true
	end
	
	oldSide = side
	
	return ok, oldSide
end

local function isAzimuthInLimits(_referencePoint, _azimuth, _targetPoint)
	local vector = ATME.C_Vector3D(_referencePoint, _targetPoint)
	local radius = 1000
	
	if vector:getHModule() > 10000 then
		radius = 0.1 * vector:getHModule()
	end
	
	local h = math.sqrt(vector:getHModule()^2 + radius^2)
	local deltaAzimuth = math.deg(math.asin(radius/h))
	local vectorHeading = vector:getAzimuth()

	local minAzimuth = vectorHeading - deltaAzimuth
	local maxAzimuth = vectorHeading + deltaAzimuth

	if maxAzimuth >= 360 then
		maxAzimuth = maxAzimuth - 360
	end
	
	if minAzimuth < 0 then
		minAzimuth = minAzimuth + 360
	end
	
	if _azimuth >= minAzimuth and _azimuth <= maxAzimuth then
		return true
	end
	
	return false
end

----------------------------------------------------------------------------------------------------------
-- DEFINITION DE LA TABLE ATME ET DES FONCTIONS GENERALES
--
-- Bien que reprenant la structure des classes définie plus loin, ATME n'est pas une classe en tant que telle.
-- ATME définit des fonctions de base et la liste des classes existantes dans ATME_Core. A ce titre, elle n'est
-- pas préfixée par C_
--
-- ATME_MT définit une métatable pour ATME (mt) et pour les extensions de modules afin d'avoir une variable globales
-- ATME.modules["nom du module"] pour les modules lancés. L'entrée "keys" interdit la création de clés nouvelles
-- dans la table ATME.
----------------------------------------------------------------------------------------------------------

local ATME_MT = {
	mt = {},
	mtModules = {},
	mtStaticObjects = {},
	keys = {
		"C_Vector3D", "C_Line2D", "C_IndexList", "C_Airbase", "C_F10Menu", "C_UserTrigger", "C_Module", "C_Smoke", "C_Flare",
		"C_StaticObjectSpawn", "C_GroupSpawnDatas", "C_Group", "C_Player", "C_AIUnit", "C_StaticObject", "C_Race", "run", "modules",
		"staticObjects",
	},
	keysStaticObjects = {}
}

ATME = {
	getLanguage = function()
		return language
	end,
	
	setF10AlphaOrder = function()
		-- setF10AlphaOrder : permet pour les menus F10 un classement alpha uniquement des items et des sous menus.
		
		C_F10Menu_flagOrder = false
	end,
	
	setF10groupIDAlphaOrder = function()
		-- setF10groupIDAlphaOrder : permet pour les menus F10 un classement par rapport au groupID des items menus puis 
		--                           alpha en cas de groupId identiques ou d'entrée de sous menus.
		
		C_F10Menu_flagOrder = true
	end,
	
	isUnitControllable = function(_unitName)
		-- isUnitControllable : Vérifie dans la mission définie si une unité est un joueur. la table allPlayerUnitsInMission
		--                      contient toutes les unités dont skill à "Player" ou "Client"
		
		return allPlayerUnitsInMission[_unitName] ~= nil
	end,
	
	isStaticObject = function(_objectName)
		-- isStaticObject : Vrai si l'objet statique existe dans la mission (et n'est pas détruit)
		
		return StaticObject.getByName(_objectName) ~= nil
	end,
	
	getGroups = function()
		-- getGroups : Donne une copie de la liste des groupes ATME actifs et non détruits
		
		local temp = {}
		for _, _group in pairs(groups) do
			temp[_group:getName()] = _group
		end
		
		return temp
	end,
	
	getAIUnits = function()
		-- getAIUnits : Donne une copie de la liste des unités AI ATME actifs et non détruites
		
		local temp = {}
		for _, _unit in pairs(AIUnits) do
			temp[_unit:getName()] = _unit
		end
		
		return temp
	end,
	
	getStaticObjects = function()
		-- getStaticObjects : Donne une copie de la liste des objets statiques (sauf FARP et airFields) actifs et non détruits
		
		local temp = {}
		for _, _staticObject in pairs(staticObjects) do
			temp[_staticObject:getName()] = _staticObject
		end
		
		return temp
	end,
	
	getPlayers = function()
		-- getPlayers : Donne une copie de la liste des unités joueurs actives et non détruites
		
		local temp = {}
		for _, _player in pairs(players) do
			temp[_player:getName()] = _player
		end
		
		return temp
	end,
	
	isPoint = function(_point)
		-- isPoint : Vérifie si la table passée en paramètre est un point et retourne POINT_BAD (0) si ce n'est pas un point :
		--           Si point 2D si présence de deux nombres _point.x et _point.y -> Retourne POINT_2D (1)
		--			 Si point 3D si présence de trois nombres _point.x, _point.y et _point.z/gModule -> Retourne POINT_3D (2)
		--           Ces nombres sont relatif au repère XYZ de DCS
		
		if type(_point) == "table" and type(_point.x) == "number" and type(_point.y) == "number" then
			if type(_point.z) == "number" then
				return POINT_3D
			elseif _point.z == nil then
				return POINT_2D
			end
		end
		
		return POINT_BAD
	end,
	
	isObjectClass = function(_object, _class)
		-- isObjectClass : Vérifie la classe d'une instance.
		
		return type(_object) == "table" and type(_class) == "table" and type(_class.getClassName) == "function" and 
              type(_object.getClassName) == "function" and _object:getClassName() == _class:getClassName()
	end,
	
	getTheatre = function()
		-- getTheatre : Retourne la carte (map) associée à la mission
		-- 			    actuellement "NEVADA" ou "CAUCASUS"
		
		return theatreName
	end,
	
	getFlag = function(_id)
		-- getFlag : Retourne la valeur d'un flag. Contrairement à la fonction de base DCS, ne gère que les flags
		--           dont l'identifiant est un nombre. C'est ainsi full compatible avec l'éditeur de mission
		
		if type(_id) == "number" then
			if _id > 0 then
			_id = math.floor(_id)
			
			return trigger.misc.getUserFlag(tostring(_id))
			else
				errorOutputCore(nil, "Function ATME.getFlag : id must be > 0")
			end
		else
			errorOutputCore(nil, "Function ATME.getFlag : id type must be number")
		end
	end,
	
	setFlag = function(_id, _value)
		-- setFlag : Modifie la valeur l'état d'un flag. Contrairement à la fonction de base DCS, ne gère que les flags
		--           dont l'identifiant est un nombre. C'est ainsi full compatible avec l'éditeur de mission. Si le flag
		--			 est modifié ici et qu'il existe un trigger dans l'éditeur sur ce même flag pour la valeur renseignée,
		--           le trigger défini dans l'éditeur de mission sera déclenché
		
		if type(_id) == "number" and (type(_value) == "number" or type(_value) == "boolean") then
			if _id > 0 then
				if (type(_value) == "number" and _value >= 0) or type(_value) == "boolean" then
					_id = math.floor(_id)
					_value = math.floor(_value)

					trigger.action.setUserFlag(tostring(_id), _value)
				else
					errorOutputCore(nil, "Function ATME.setFlag : value must be > 0 or true/false")
				end
			else
				errorOutputCore(nil, "Function ATME.setFlag : id must be > 0")
			end
		else
			if type(_id) ~= "number" then
				errorOutputCore(nil, "Function ATME.setFlag : id type must be number")
			elseif type(_value) ~= "number" and type(_value) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.setFlag : value type must be number or boolean")
			end
		end
	end,
	
	getDCSZone = function(_name)
		-- getDCSZone : Retourne une zone définie dans l'éditeur de mission, nil si inexistante.
		
		if type(_name) == "string" then
			return trigger.misc.getZone(_name) 
		else
			errorOutputCore(nil, "Function ATME.getDCSZone : name type must be string")
		end
	end,
	
	getTime = function()
		-- getTime : Retourne le temps de la mission.
		
		return timer.getTime()
	end,
	
	loopTransmission = function(_file, _point, _modulation, _frequency, _power)
		-- loopTransmission : Active une émission radio en boucle. Le fichier son est lu, puis à la fin reprend
		--                    au début.
		local isPoint = ATME.isPoint(_point)
		
		if type(_file) == "string" and isPoint ~= POINT_BAD and 
		   type(_modulation) == "string" and type(_frequency) == "number" and type(_power) == "number" then
			if _frequency > 0 then
				if _power > 0 then
					local newRadioTransmission = initRadioTransmission(_file, _point, _modulation, _frequency, _power, "")
					newRadioTransmission.always = true
					
					startDCSRadioTransmission(newRadioTransmission.file, newRadioTransmission.point, newRadioTransmission.modulation,
											true, newRadioTransmission.frequency, newRadioTransmission.power, 
											newRadioTransmission.textId)

					return newRadioTransmission.entry
				else
					errorOutputCore(nil, "Function ATME.loopTransmission : power must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.loopTransmission : frequency must be > 0")
			end
		else
			if isPoint == POINT_BAD then
				errorOutputCore(nil, "Function ATME.loopTransmission : point type must be table (x,y) or (x,y,z)")
			elseif type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.loopTransmission : file type must be string")
			elseif type(_modulation) ~= "string" then
				errorOutputCore(nil, "Function ATME.loopTransmission : modulation type must be string")
			elseif type(_frequency) ~= "number" then
				errorOutputCore(nil, "Function ATME.loopTransmission : frequency type must be number")
			elseif type(_power) ~= "number" then
				errorOutputCore(nil, "Function ATME.loopTransmission : power type must be number")
			end
		end
	end,
	
	sendTransmissionOnce = function(_file, _point, _modulation, _frequency, _power)
		-- sendTransmissionOnce : Active une seule émission radio
		
		local isPoint = ATME.isPoint(_point)
		
		if type(_file) == "string" and isPoint ~= POINT_BAD and 
		   type(_modulation) == "string" and type(_frequency) == "number" and type(_power) == "number" then
			if _frequency > 0 then
				if _power > 0 then
					local newRadioTransmission = initRadioTransmission(_file, _point, _modulation, _frequency, _power, "")
					
					startDCSRadioTransmission(newRadioTransmission.file, newRadioTransmission.point , newRadioTransmission.modulation, 
									false, newRadioTransmission.frequency, newRadioTransmission.power, newRadioTransmission.textId)
					
					-- Transmission unique, on remove de suite pour ne pas "poluer" la gestion dans mainTimer
					activatedRadioTransmissions:remove(newRadioTransmission.entry)
				else
					errorOutputCore(nil, "Function ATME.sendTransmissionOnce : power must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.sendTransmissionOnce : frequency must be > 0")
			end
		else
			if isPoint == POINT_BAD then
				errorOutputCore(nil, "Function ATME.sendTransmissionOnce : point type must be table (x,y) or (x,y,z)")
			elseif type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.sendTransmissionOnce : file type must be string")
			elseif type(_modulation) ~= "string" then
				errorOutputCore(nil, "Function ATME.sendTransmissionOnce : modulation type must be string")
			elseif type(_frequency) ~= "number" then
				errorOutputCore(nil, "Function ATME.sendTransmissionOnce : frequency type must be number")
			elseif type(_power) ~= "number" then
				errorOutputCore(nil, "Function ATME.sendTransmissionOnce : power type must be number")
			end
		end
	end,
	
	startTransmission = function(_file, _point, _modulation, _frequency, _power, _interval)
		-- startTransmission : Active une émission radio dont le fichier son est lu à intervalle régulier. Si l'intervalle
		--                     est plus court que la durée du fichier son, la diffusion du fichier sera interrompue. 
		--					   Tout est géré par ATME. Un intervalle égal à 0 engendre une diffusion unique (cf la gestion mainTimer).
		
		local isPoint = ATME.isPoint(_point)
		
		if type(_file) == "string" and isPoint ~= POINT_BAD and 
		   type(_modulation) == "string" and type(_frequency) == "number" and type(_power) == "number" and type(_interval) == "number" then
			if _frequency > 0 then
				if _power > 0 then
					if _interval >= 1 then
						local newRadioTransmission = initRadioTransmission(_file, _point, _modulation, _frequency, _power, "")
						newRadioTransmission.interval = math.floor(_interval)
						newRadioTransmission.currentTip = 1
						newRadioTransmission.always = false
						
						startDCSRadioTransmission(newRadioTransmission.file, newRadioTransmission.point , newRadioTransmission.modulation, 
										false, newRadioTransmission.frequency, newRadioTransmission.power, newRadioTransmission.textId)
						
						return newRadioTransmission.entry
					else
						errorOutputCore(nil, "Function ATME.startTransmission : interval must be >= 1")
					end
				else
					errorOutputCore(nil, "Function ATME.startTransmission : power must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.startTransmission : frequency must be > 0")
			end
		else
			if isPoint == POINT_BAD then
				errorOutputCore(nil, "Function ATME.startTransmission : point type must be table (x,y) or (x,y,z)")
			elseif type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.startTransmission : file type must be string")
			elseif type(_modulation) ~= "string" then
				errorOutputCore(nil, "Function ATME.startTransmission : modulation type must be string")
			elseif type(_frequency) ~= "number" then
				errorOutputCore(nil, "Function ATME.startTransmission : frequency type must be number")
			elseif type(_power) ~= "number" then
				errorOutputCore(nil, "Function ATME.startTransmission : power type must be number")
			elseif type(_interval) ~= "number" then
				errorOutputCore(nil, "Function ATME.startTransmission : interval type must be number")
			end
		end
	end,
	
	stopTransmission = function(_entry)
		-- stopTransmission : Arrête une émission radio en boucle ou émise à intervalle régulier. Pour une émission
		--                    en boucle, l'arrêt sera immédiat. Pour une diffusion à intervalle régulier, le fichier sera
		--                    lu complètement avant l'arrêt.
		
		if type(_entry) == "number" then
			if _entry >= 1 then
				local entry = math.floor(_entry)
				local radioTransmission = activatedRadioTransmissions:get(entry)
				if radioTransmission ~= nil and radioTransmission.unitName == "" then  
					-- La radioTransmission ne doit pas être rattachée à une unité
					
					if radioTransmission.always == true then
						stopDCSRadioTransmission(radioTransmission.textId)
					end
					
					activatedRadioTransmissions:remove(entry)
				end
			else
				errorOutputCore(nil, "Function ATME.stopTransmission : entry must be >= 1")
			end
		else
			errorOutputCore(nil, "Function ATME.stopTransmission : entry type must be number")
		end
	end,
	
	explode = function(_point, _power)
		-- explode : Génère une explosion au point indiqué.
		
		if  ATME.isPoint(_point) ~= POINT_BAD and type(_power) == "number" then
			if _power >= 0 then
				local point = ATME.convertToPoint3D(_point)
				
				trigger.action.explosion(point, _power)
			else
				errorOutputCore(nil, "Function ATME.explode : power must be >= 0")
			end
		else
			if ATME.isPoint(_point) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.explode : point type must be table (x,y) or (x,y,z)")
			elseif type(_power) ~= "number" then
				errorOutputCore(nil, "Function ATME.explode : power type must be number")
			end
		end
	end,
	
	displayForAll = function(_text, _duration)
		-- displayForAll : Affiche un message à tous les joueurs actifs avec un durée donnée
		
		if type(_text) == "string" and type(_duration) == "number" then
			if _duration >= 0 then
				trigger.action.outText(_text, _duration)
			else
				errorOutputCore(nil, "Function ATME.displayForAll : duration type must be >= 0")
			end
		else
			if type(_text) ~= "string" then
				errorOutputCore(nil, "Function ATME.displayForAll : text type must be string")
			elseif type(_duration) ~= "number" then
				errorOutputCore(nil, "Function ATME.displayForAll : duration type must be string")
			end
		end
	end,
	
	soundForAll = function(_file)
		-- soundForAll : Lit un fichier son pour tous les joueurs actifs
		
		if type(_file) == "string" then
			trigger.action.outSound(_file)
		else
			errorOutputCore(nil, "Function ATME.soundForAll : file type must be string")
		end
	end,
	
	displayForCoalition = function(_coalition, _text, _duration)
		-- displayForCoalition : Affiche un message à tous les joueurs actifs d'une coalition avec une durée donnée
		--                       La coalition doit être "NEUTRAL", "RED" ou "BLUE"
		
		if type(_coalition) == "string" and type(_text) == "string" and type(_duration) == "number" then
			if _duration >= 0 then
				if coalitionIds[_coalition] ~= nil then
					trigger.action.outTextForCoalition(coalitionIds[_coalition], _text, _duration)
				else
					errorOutputCore(nil, "Function ATME.displayForCoalition : coalition bad name")
				end
			else
				errorOutputCore(nil, "Function ATME.displayForCoalition : duration type must be >= 0")
			end
		else
			if type(_coalition) ~= "string" then
				errorOutputCore(nil, "Function ATME.displayForCoalition : coalition type must be string")
			elseif type(_text) ~= "string" then
				errorOutputCore(nil, "Function ATME.displayForCoalition : text type must be string")
			elseif type(_duration) ~= "number" then
				errorOutputCore(nil, "Function ATME.displayForCoalition : duration type must be string")
			end
		end
	end,
	
	soundForCoalition = function(_coalition, _file)
		-- soundForCoalition : Lit un fichier son pour tous les joueurs actifs d'une coalition
		--                     La coalition doit être "NEUTRAL", "RED" ou "BLUE"
		
		if type(_coalition) == "string" and type(_file) == "string" then
			if coalitionIds[_coalition] ~= nil then
				trigger.action.outSoundForCoalition(coalitionIds[_coalition], _file)
			else
				errorOutputCore(nil, "Function ATME.soundForCoalition : coalition bad name")
			end
		else
			if type(_coalition) ~= "string" then
				errorOutputCore(nil, "Function ATME.soundForCoalition : coalition type must be string")
			elseif type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.soundForCoalition : file type must be string")
			end
		end
	end,
	
	displayForCountry = function(_country, _text, _duration)
		-- displayForCoalition : Affiche un message à tous les joueurs actifs d'une coalition avec une durée donnée
		--                       La nation est celle définie par DCS.
		
		if type(_country) == "string" and type(_text) == "string" and type(_duration) == "number" then
			if _duration >= 0 then
				_country = string.upper(_country)
				if country.id[_country] ~= nil then
					trigger.action.outTextForCountry(country.id[_country], _text, _duration, false)
				else
					errorOutputCore(nil, "Function ATME.displayForCountry : country bad name")
				end
			else
				errorOutputCore(nil, "Function ATME.displayForCountry : duration type must be >= 0")
			end
		else
			if type(_country) ~= "string" then
				errorOutputCore(nil, "Function ATME.displayForCountry : country type must be string")
			elseif type(_text) ~= "string" then
				errorOutputCore(nil, "Function ATME.displayForCountry : text type must be string")
			elseif type(_duration) ~= "number" then
				errorOutputCore(nil, "Function ATME.displayForCountry : duration type must be string")
			end
		end
	end,
	
	soundForCountry = function(_country, _file)
		-- soundForCoalition : Lit un fichier son pour tous les joueurs actifs d'une nation.
		--                     La nation est celle définie par DCS.
		
		if type(_country) == "string" and type(_file) == "string" then
			_country = string.upper(_country)
			if country.id[_country] ~= nil then
				trigger.action.outSoundForCountry(country.id[_country], _file)
			else
				errorOutputCore(nil, "Function ATME.soundForCountry : country bad name")
			end
		else
			if type(_country) ~= "string" then
				errorOutputCore(nil, "Function ATME.soundForCountry : country type must be string")
			elseif type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.soundForCountry : file type must be string")
			end
		end
	end,
	
	setDebugLevel = function(_level)
		-- setDebugLevel : Définit le niveau de débug. Tout module en mode debug est concerné. 
		--				   Les modules en mode normal ne sont pas concernés
		--                 Ce niveau permet de filtre l'affichage des messages d'erreur pour les niveaux supérieurs ou égaux
		--                (niveau 1, le plus haut à niveau 9 le plus bas).
		
		if type(_level) == "number" then
			if _level >= 0 then
				debugLevel = math.floor(_level)
			else
				errorOutputCore(nil, "Function ATME.setDebugLevel : level type must be >= 0")
			end
		else
			errorOutputCore(nil, "Function ATME.setDebugLevel : level type must be number")
		end
	end,
	
	isSurfaceWater = function(_point)
		-- isSurfaceWater : Vérifie si un point est au dessous de l'eau
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint ~= POINT_BAD then
			if isPoint == POINT_3D then
				_point.y = _point.z
			end
			
			local landType = land.getSurfaceType(_point)
			
			return landType == land.SurfaceType.WATER or landType == land.SurfaceType.SHALLOW_WATER
		else
			errorOutputCore(nil, "Function ATME.isSurfaceWater : point type must be table (x,y) or (x,y,z)")
		end
	end,
	
	isSurfaceRunway = function(_point)
		-- isSurfaceRunway : Vérifie si un point est au dessous d'une piste ou d'un taxiway
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint ~= POINT_BAD then
			if isPoint == POINT_3D then
				_point.y = _point.z
			end
			local landType = land.getSurfaceType(_point)
			
			return landType == land.SurfaceType.RUNWAY
		else
			errorOutputCore(nil, "Function ATME.isSurfaceRunway : point type must be table (x,y) or (x,y,z)")
		end
	end,
	
	isSurfaceLand = function(_point)
		-- isSurfaceRunway : Vérifie si un point est au dessous de la terren hors piste ou taxiway
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint ~= POINT_BAD then
			if isPoint == POINT_3D then
				_point.y = _point.z
			end
			local landType = land.getSurfaceType(_point)
			
			return landType == land.SurfaceType.LAND
		else
			errorOutputCore(nil, "Function ATME.isSurfaceLand : point type must be table (x,y) or (x,y,z)")
		end
	end,
	
	isSurfaceRoad = function(_point)
		-- isSurfaceRoad : Vérifie si un point est au dessous de la terren hors piste ou taxiway
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint ~= POINT_BAD then
			if isPoint == POINT_3D then
				_point.y = _point.z
			end
			local landType = land.getSurfaceType(_point)
			
			return landType == land.SurfaceType.ROAD
		else
			errorOutputCore(nil, "Function ATME.isSurfaceRoad : point type must be table (x,y) or (x,y,z)")
		end
	end,

	getSurfaceAltitude = function(_point)
		-- getSurfaceAltitude : Donne l'altitude du terrain à la verticale du point
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint ~= POINT_BAD then
			if isPoint == POINT_3D then
				return land.getHeight({x = _point.x, y = _point.z})
			else
				return land.getHeight({x = _point.x, y = _point.y})
			end
		else
			errorOutputCore(nil, "Function ATME.getSurfaceAltitude : point type must be table (x,y) or (x,y,z)")
		end
	end,
	
	getPointAltitude = function(_point)
		-- getPointAltitude : Donne l'altitude du point. Si le point est 2D, ce sera
		--					  l'altitude du terrain
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint ~= POINT_BAD then
			if isPoint == POINT_3D then
				return _point.y
			else
				return getSurfaceAltitude(_point)
			end
		else
			errorOutputCore(nil, "Function ATME.getPointAltitude : point type must be table (x,y) or (x,y,z)")
		end
	end,
	
	convertToPoint3D = function(_point)
		-- Permet de convertir un point en point 3D
		-- afin de s'assurer que le point retourné est bien 3D.
		
		if ATME.isPoint(_point) ~= POINT_BAD then
			local point = {}
			
			point.x = _point.x
			if ATME.isPoint(_point) == POINT_2D then
				point.z = _point.y
				point.y = ATME.getSurfaceAltitude(_point)
			else
				point.y = _point.y
				point.z = _point.z
			end
			
			return point
		else
			errorOutputCore(nil, "Function ATME.convertToPoint3D : point type must be table (x,y) or (x,y,z)")
		end
	end,
	
	arePointsVisible = function(_pA, _pB)
		-- arePointsVisible : Vérifie si deux points sont mutuellement visibles (ie non masqués) par le terrain
		
		local isPointA = ATME.isPoint(_pA)
		local isPointB = ATME.isPoint(_pB)
		
		if isPointA ~= POINT_BAD and isPointB ~= POINT_BAD then
			local pA = ATME.convertToPoint3D(_pA)
			local pB = ATME.convertToPoint3D(_pB)
			
			-- Ajout d'un offset sur l'altitude pour éviter un pb si l'objet est sur le 
			-- sol avec une altitude dans le sol (cf doc DCS pour land.isVisible)
			pA.y = pA.y + 3
			pB.y = pB.y + 3
			return land.isVisible(pA, pB)
		else
			if isPointA == POINT_BAD then
				errorOutputCore(nil, "Function ATME.arePointsVisible : pA type must be table (x,y) or (x,y,z)")
			elseif isPointB == POINT_BAD then
				errorOutputCore(nil, "Function ATME.arePointsVisible : pB type must be table (x,y) or (x,y,z)")
			end
		end
	end,
	
	getWindVector = function(_point, _withTurbulence)
		-- getWindVector : Retourne un instance de ATME.C_Vector3D correspond au vecteur vent
		--                 avec ou sans turbulences
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint == POINT_3D and type(_withTurbulence) == "boolean" then
			if _withTurbulence == true then
				local wind = atmosphere.getWindWithTurbulence(_point)
				return ATME.C_Vector3D(wind) 
			else
				local wind = atmosphere.getWind(_point)
				return ATME.C_Vector3D(wind) 
			end
		else
			if isPoint == POINT_BAD then
				errorOutputCore(nil, "Function ATME.getWindVector : point type must be table (x,y) or (x,y,z)")
			elseif type(_withTurbulence) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.getWindVector : withTurbulence type must be boolean")
			end
		end
	end,
	
	getWindSpeed = function(_point, _withTurbulence)
		-- getWindSpeed : Retourne la vitesse du vent calculée sur les 3 dimensions
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint == POINT_3D and type(_withTurbulence) == "boolean" then
			local wind = ATME.getWindVector(_point, _withTurbulence)

			return wind:getModule()
		else
			if isPoint == POINT_BAD then
				errorOutputCore(nil, "Function ATME.getWindSpeed : point type must be table (x,y) or (x,y,z)")
			elseif type(_withTurbulence) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.getWindSpeed : withTurbulence type must be boolean")
			end
		end
	end,
	
	getWindHSpeed = function(_point, _withTurbulence)
		-- getWindSpeed : Retourne la vitesse horizontale du vent calculée sur les 2 dimensions x et z
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint == POINT_3D and type(_withTurbulence) == "boolean" then
			local wind = ATME.getWindVector(_point, _withTurbulence)

			return wind:getHModule()
		else
			if isPoint == POINT_BAD then
				errorOutputCore(nil, "Function ATME.getWindHSpeed : point type must be table (x,y) or (x,y,z)")
			elseif type(_withTurbulence) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.getWindHSpeed : withTurbulence type must be boolean")
			end
		end
	end,
	
	getWindAzimuth = function(_point, _withTurbulence)
		-- getWindAzimuth : Retourne l'azimuth du vent (attention pas son origine mais sa direction)
		
		local isPoint = ATME.isPoint(_point)
		if  isPoint == POINT_3D and type(_withTurbulence) == "boolean" then
			local wind = ATME.getWindVector(_point, _withTurbulence)

			return wind:getAzimuth()
		else
			if isPoint == POINT_BAD then
				errorOutputCore(nil, "Function ATME.getWindAzimuth : point type must be table (x,y) or (x,y,z)")
			elseif type(_withTurbulence) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.getWindAzimuth : withTurbulence type must be boolean")
			end
		end
	end,
		
	convertMtoNM = function(_distance)
		-- convertMtoNM : Convertit des mètres en miles nautiques
		
		if type(_distance) == "number" then
			return _distance * 0.000539957
		else
			errorOutputCore(nil, "Function ATME.convertMtoNM : distance type must be number")
		end
	end,

	convertNMtoM = function(_distance)
		-- convertNMtoM : Convertit des miles nautiques en mètres
		
		if type(_distance) == "number" then
			return _distance / 0.000539957
		else
			errorOutputCore(nil, "Function ATME.convertNMtoM : distance type must be number")
		end
	end,
	
	convertMtoFts = function(_distance)
		-- convertMtoFts : Convertit des mètres en pieds
		
		if type(_distance) == "number" then
			return _distance * 3.28084
		else
			errorOutputCore(nil, "Function ATME.convertMtoFts : distance type must be number")
		end	
	end,
	
	convertFtstoM =  function(_distance)
		-- convertFtstoM : Convertit des pieds en mètres
		
		if type(_distance) == "number" then
			return _distance / 3.28084
		else
			errorOutputCore(nil, "Function ATME.convertFtstoM : distance type must be number")
		end
	end,
	
	convertMpstoNds = function(_speed)
		-- convertMpstoNds : Convertit des m/s en noeuds
		
		if type(_speed) == "number" then
			return _speed * 1.9438
		else
			errorOutputCore(nil, "Function ATME.convertMpstoNds : speed type must be number")
		end
	end,
	
	convertNdstoMps = function(_speed)
		-- convertNdstoMps : Convertit des noeuds en m/s
		
		if type(_speed) == "number" then
			return _speed / 1.9438
		else
			errorOutputCore(nil, "Function ATME.convertNdstoMps : speed type must be number")
		end
	end,

	convertMpstoKph = function(_speed)
		-- convertMpstoKph : Convertit des m/s en km/h
		
		if type(_speed) == "number" then
			return _speed * 3.6
		else
			errorOutputCore(nil, "Function ATME.convertMpstoKph : speed type must be number")
		end
	end,

	convertKphtoMps = function(_speed)
		-- convertKphtoMps : Convertit des km/h en m/s
		
		if type(_speed) == "number" then
			return _speed / 3.6
		else
			errorOutputCore(nil, "Function ATME.convertKphtoMps : speed type must be number")
		end
	end,
	
	convertToDMS = function(_degrees, _secdec)
		-- convertToDMS : Convertit des angles en degrés décimaux soit :
		--                - en degrés/minutes/secondes si _secdec vaut false
		--                - en degrés/minutes/décimales si _secdec vaut true
		
		if type(_degrees) == "number" and type(_secdec) == "boolean" then
			local dir = 1
			if _degrees < 0 then
				dir = -1
			end
			
			local dabs = math.abs(_degrees)
			local m = (dabs - math.floor(dabs)) * 60
			local mfrac = m - math.floor(m)
			local s = mfrac * 60
			
			if _secdec == true then
				s = mfrac * 100
			end
			
			return dir, math.floor(dabs), math.floor(m), math.floor(s)
		else
			if type(_degrees) ~= "number" then
				errorOutputCore(nil, "Function ATME.convertToDMS : degrees type must be number")
			elseif type(_secdec) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.convertToDMS : secdec type must be boolean")
			end
		end
	end,
	
	convertToHMSd = function (_seconds)
		-- convertToHMSd : Convertit un temps en secondes en heures/minutes/secondes/100ème
		-- En cas de temps négatif, retourne une erreur simple.
		
		if type(_seconds) == "number" then
			if _seconds >= 0 then	
				local h = math.floor(_seconds/3600)
				local m =  math.floor((_seconds - h * 3600)/60)
				local s = math.floor(_seconds - h * 3600 - m * 60)
				local d = (_seconds - math.floor(_seconds)) * 100
				return false, h, m, s, d
			else -- Erreur
				return true
			end
		else
			errorOutputCore(nil, "Function ATME.convertToHMSd : seconds type must be number")
		end
	end,

	convertPointToLL = function(_point)
		-- convertPointToLL : Convertit un point en coordonnées latitude/longitude/altitude
		
		if ATME.isPoint(_point) ~= POINT_BAD then
			local point = {}
			
			point.x = _point.x
			if ATME.isPoint(_point) == POINT_2D then
				point.z = _point.y
				point.y = ATME.getSurfaceAltitude(_point)
			else
				point.y = _point.y
				point.z = _point.z
			end
			
			return coord.LOtoLL(point)
		else
			errorOutputCore(nil, "Function ATME.convertPointToLL : point type must be table (x,y) or (x,y,z)")
		end
	end,
	
	convertLLToPoint = function(_latitude, _longitude, _altitude)
		-- convertLLToPoint : Convertit un triplet latitude/longitude/altitude en un point 3D
		
		if type(_latitude) == "number" and type(_longitude) == "number" and type(_altitude) == "number" then
			return coord.LLtoLO(_latitude, _longitude, _altitude)
		else
			if type(_latitude) ~= "number" then
				errorOutputCore(nil, "Function ATME.convertLLToPoint : latitude type must be number")
			elseif type(_longitude) ~= "number" then
				errorOutputCore(nil, "Function ATME.convertLLToPoint : longitude type must be number")
			elseif type(_altitude) ~= "number" then
				errorOutputCore(nil, "Function ATME.convertLLToPoint : altitude type must be number")
			end
		end
	end,
	
	convertLLToMGRS = function(_latitude, _longitude)
		-- convertLLToMGRS : Convertit une latitude/longitude en référence MGRS
		
		if type(_latitude) == "number" and type(_longitude) == "number" then
			return coord.LLtoMGRS(_latitude, _longitude)
		else
			if type(_latitude) ~= "number" then
				errorOutputCore(nil, "Function ATME.convertLLToMGRS : latitude type must be number")
			elseif type(_longitude) ~= "number" then
				errorOutputCore(nil, "Function ATME.convertLLToMGRS : longitude type must be number")
			end
		end
	end,
	
	convertMGRSToLL = function(_tableMGRS)
		-- convertMGRSToLL : Convertit une référence MGRS en latitude/longitude
		
		if type(_tableMGRS) == "table" and type(table.UTMZone) == "string" and type(table.MGRSDigraph) == "string" and type(table.Easting) == "number" and type(table.Northing ) == "number" then
			return coord.MGRStoLL(_tableMGRS)
		else
			errorOutputCore(nil, "Function ATME.convertMGRSToLL : tableMGRS must have MGRS format")
		end
	end,
	
	scalaireHFromPoints = function (_A, _B, _C)
		-- scalaireHFromPoints : Calcule le produit scalaire des vecteurs AB et AC, sur leurs coordonnées x,z
		
		if ATME.isPoint(_A) ~= POINT_BAD and ATME.isPoint(_B) ~= POINT_BAD and ATME.isPoint(_C) ~= POINT_BAD then
			local vectAB = ATME.C_Vector3D(_A, _B)
			local vectAC = ATME.C_Vector3D(_A, _C)
			
			return vectAB:getX() * vectAC:getX() + vectAB:getZ() * vectAC:getZ()
		else
			if ATME.isPoint(_A) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.scalaireHFromPoints : A type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_B) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.scalaireHFromPoints : B type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_C) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.scalaireHFromPoints : C type must be table (x,y) or (x,y,z)")
			end
		end
	end,
	
	whichSide = function (_A, _B, _C)
		-- whichSide : Vérifie si un point C est à droite ou à gauche d'un axe orienté AB :
		--               - 0 si le point est sur la droite
		--				 - 1 si le point est à droite
		--               - -1 si le point est à gauche
		
		if ATME.isPoint(_A) ~= POINT_BAD and ATME.isPoint(_B) ~= POINT_BAD and ATME.isPoint(_C) ~= POINT_BAD then
			local det
			
			local vectAB = ATME.C_Vector3D(_A, _B)
			local vectAC = ATME.C_Vector3D(_A, _C)
			
			det = vectAB:getX() * vectAC:getZ() - vectAB:getZ() * vectAC:getX()
			
			-- det < 0, C est à gauche de la droite AB
			-- det > 0,  C est à droite de la droite AB
			-- det = 0, C est dans sur la droite AB
			
			if det ~= nil then
				if det > 0 then
					return 1
				elseif det < 0 then
					return -1
				else
					return 0
				end
			end
		else
			if ATME.isPoint(_A) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.whichSide : A type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_B) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.whichSide : B type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_C) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.whichSide : C type must be table (x,y) or (x,y,z)")
			end
		end
	end,
	
	rotationH =  function (_point, _center, _angle)
		-- rotationH : Calcule la rotation horizontale d'un point par rapport à un centre et un angle (référence nord)
		--			   L'angle est en degrés. A noter : le repère est inversé x en vertical et repère xOy indirect (norme DCS)
		
		if ATME.isPoint(_point) ~= POINT_BAD and ATME.isPoint(_center) ~= POINT_BAD and type(_angle) == "number" then
			local angle = math.rad(_angle)
			local point = ATME.convertToPoint3D(_point)
			local center = ATME.convertToPoint3D(_center)
			
			return {
				x = center.x + (point.x - center.x) * math.cos(angle) + (point.z - center.z) * math.sin(angle),
				y = point.y,
				z = center.z - (point.x - center.x) * math.sin(angle) + (point.z - center.z) * math.cos(angle),
			}
		else
			if ATME.isPoint(_point) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.rotationH : point type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_center) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.rotationH : center type must be table (x,y) or (x,y,z)")
			elseif type(_angle) ~= "number" then
				errorOutputCore(nil, "Function ATME.rotationH : angle type must be number")
			end
		end
	end,
}

setmetatable(ATME, ATME_MT.mt)

local C_Module_MT -- Prédéclaration de C_Module_MT

ATME_MT.mt.__metatable = "Private"
ATME_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(ATME_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil,"ATME : Bad key " .. k)
	end
end

ATME.staticObjects = {
	createCargo = function(_countryName, _staticObjectName, _typeCargo, _position, _mass, _isDead)
		if type(_countryName) == "string" and type(_staticObjectName) == "string" and type(_typeCargo) == "string" and (type(_position) == "string" or ATME.isPoint(_position) ~= POINT_BAD) and
				type(_mass) == "number" and type(_isDead) == "boolean" then
			if countries[_countryName] ~= nil then
				if ATME.C_StaticObject.exists(_staticObjectName) == false then
					local point
					if type(_position) == "string" then
						local zone = ATME.getDCSZone(_position)
						if zone ==  nil then
							errorOutputCore(nil, "Function ATME.staticObjects.createCargo : DCS zone " .. _position .. " unknown")
							return
						end
						
						point = {}			
						local angle = 2 * math.pi * math.random()
						local radius = math.random(0, zone.radius)
						point.x = zone.point.x + radius * math.sin(angle)
						point.z = zone.point.z + radius * math.cos(angle)
					else
						point = ATME.convertToPoint3D(_position) 
					end
					if cargos[_typeCargo] ~= nil then
						local typeCargo = cargos[_typeCargo].type
						local shape = cargos[_typeCargo].shape
						local rateSpecial = cargos[_typeCargo].rate
						if rateSpecial == nil then
							rateSpecial = 100
						end
						
						if _mass > 0 then
							-- L'objet statique est ajouté aux objets de la mission
							-- contrairement aux AIUnit et Players.
							
							newStaticObjectDatas = {
								name = _staticObjectName,
								type = typeCargo,
								x = point.x,
								y = point.z,
								canCargo = true,
								heading = math.random(0,359),
								category = "Cargos",
								dead = _isDead,
								shape_name = shape,
								mass = math.floor(_mass),
								rate = rateSpecial,
							}
							
							-- Permet de transmettre la catégorie lors de la création de l'objet. A ce moment
							-- là sera remis à nil
							newStaticObjectsCategory[_staticObjectName] = "Cargos"
							
							coalition.addStaticObject(countries[_countryName], newStaticObjectDatas)
						else
							errorOutputCore(nil, "Function ATME.staticObjects.createCargo : mass must be > 0")
						end
					else
						errorOutputCore(nil, "Function ATME.staticObjects.createCargo : typeCargo must exist")
					end
				else
					errorOutputCore(nil, "Function ATME.staticObjects.createCargo : Static object " .. _staticObjectName .. "  already exists")
				end
			else
				errorOutputCore(nil, "Function ATME.staticObjects.createCargo : Country " .. _countryName .. "  unknown")
			end
		else
			if type(_countryName) ~= "string" then 
				errorOutputCore(nil, "Function ATME.staticObjects.createCargo : countryName type must be string")
			elseif type(_staticObjectName) ~= "string" then 
				errorOutputCore(nil, "Function ATME.staticObjects.createCargo : staticObjectName type must be string")
			elseif type(_typeCargo) ~= "string" then 
				errorOutputCore(nil, "Function ATME.staticObjects.createCargo : typeCargo type must be string")
			elseif ATME.isPoint(_position) == POINT_BAD and type(_position) ~= string then
				errorOutputCore(nil, "Function ATME.staticObjects.createCargo : position type must be string or table (x,y) or (x,y,z)")
			elseif type(_mass) ~= "number" then 
				errorOutputCore(nil, "Function ATME.staticObjects.createCargo : mass type must be number")
			elseif type(_isDead) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.staticObjects.createCargo : isDead type must be boolean")
			end
		end
	end,
	
	createWhiteContainer = function(_countryName, _staticObjectName, _position, _heading, _isDead)
		if type(_countryName) == "string" and type(_staticObjectName) == "string" and (type(_position) == "string" or ATME.isPoint(_position) ~= POINT_BAD) and
				type(_heading) == "number" and type(_isDead) == "boolean" then
			if countries[_countryName] ~= nil then
				if ATME.C_StaticObject.exists(_staticObjectName) == false then
					local point
					if type(_position) == "string" then
						local zone = ATME.getDCSZone(_position)
						if zone ==  nil then
							errorOutputCore(nil, "Function ATME.staticObjects.createCargo : DCS zone " .. _position .. " unknown")
							return
						end

						point = {}
						local angle = 2 * math.pi * math.random()
						local radius = math.random(0, zone.radius)
						point.x = zone.point.x + radius * math.sin(angle)
						point.z = zone.point.z + radius * math.cos(angle)
					else
						point = ATME.convertToPoint3D(_position) 
					end
					if _heading >= 0 and _heading <= 359 then
						newStaticObjectDatas = {
							name = _staticObjectName,
							type = "Container white",
							x = point.x,
							y = point.z,
							canCargo = false,
							heading = math.floor(_heading),
							category = "Fortifications",
							dead = _isDead,
							shape_name = "konteiner_white",
							rate = 100,
						}
						
						-- Permet de transmettre la catégorie lors de la création de l'objet. A ce moment
						-- là sera remis à nil
						newStaticObjectsCategory[_staticObjectName] = "Fortifications"
						
						coalition.addStaticObject(countries[_countryName], newStaticObjectDatas)
					else
						errorOutputCore(nil, "Function ATME.staticObjects.createWhiteContainer : heading must be >= 0 and <= 359")
					end
				else
					errorOutputCore(nil, "Function ATME.staticObjects.createWhiteContainer : Static object " .. _staticObjectName .. "  already exists")
				end
			else
				errorOutputCore(nil, "Function ATME.staticObjects.createWhiteContainer : Country " .. _countryName .. "  unknown")
			end
		else
			if type(_countryName) ~= "string" then 
				errorOutputCore(nil, "Function ATME.staticObjects.createWhiteContainer : countryName type must be string")
			elseif type(_staticObjectName) ~= "string" then 
				errorOutputCore(nil, "Function ATME.staticObjects.createWhiteContainer : staticObjectName type must be string")
			elseif ATME.isPoint(_position) == POINT_BAD and type(_position) ~= string then
				errorOutputCore(nil, "Function ATME.staticObjects.createWhiteContainer : position type must be string or table (x,y) or (x,y,z)")
			elseif type(_heading) ~= "number" then
				errorOutputCore(nil, "Function ATME.staticObjects.createWhiteContainer : heading type must be number")
			elseif type(_isDead) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.staticObjects.createWhiteContainer : isDead type must be boolean")
			end
		end
	end,
}

setmetatable(ATME.staticObjects, ATME_MT.mtStaticObjects)
ATME_MT.mtStaticObjects.__metatable = "Private"
ATME_MT.mtStaticObjects.__newindex = function(t, k, v)
	local found = false
	
	for _, _localModuleMembers in ipairs(ATME_MT.keysStaticObjects) do
		if _localModuleMembers.name == k then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil,"ATME : ATME.staticObjects Bad key " .. k)
	end
end


ATME.modules = {}  -- Déclaration de ATME.modules pour les variables globales des modules

setmetatable(ATME.modules, ATME_MT.mtModules)
ATME_MT.mtModules.__metatable = "Private"
ATME_MT.mtModules.__newindex = function(t, k, v)
	local found = false
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if _localModuleMembers.name == k then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil,"ATME : ATME.modules Bad key " .. k)
	end
end



----------------------------------------------------------------------------------------------------------
-- DEFINITION DES CLASSES D'OBJETS DEFINIS DANS ATME
--
-- Quelques points essentiels :
-- 			- Une classe d'objet est une table lua particulière, préfixée de "C_"
--			- Une instance d'objet est un objet créé à partir d'une classe, c'est donc aussi une table qui peut
--			  être cependant étendue par le création de méthodes dans le constructeur local C_Class_create.
--			- Les fonctions liées à une classe et non à une instance sont définies dans la classe.
--			- Une classe d'objet a également une classe spéciale suffixée par _MT et locale à ATME_Core. Cette
--            classe définit : 
--				-> la metatable des instances (entrée "mt")
--				-> l'entrée "keys" des instances pour en interdire la création, lors de la création de l'instance. En effet,
--                 le constructeur local C_Class_create crée l'instance et lui associe des méthodes spécifique pour  
--				   les variables locales (closure). Ce sont ces méthodes, non gérées par la metaclasse qui sont listées
--				   dans l'entrée "keys" pour le contrôle des entrées de la table représentant l'instance.
--				-> le nom de la classe entrée "className"
--				-> la création du constructeur "public" de la classe (__call). Par défaut seule la fonction locale
--                 C_Class_create est créée ce qui interdit toute création d'instance dans les modules externes. C'est
--				   le cas pour les classes C_Group (Groupes au format ATME), C_Player (unités joueurs au format ATME),
--                 C_AIUnit (Unités AI au format ATME) et C_StaticObject (objets statiques au format ATME sauf FARP et
--				   Airfields.
--				-> Certaines propriétés dont l'accès ne doit être que local et dont l'accès à la modification pourrait
--  			   engendrer des incohérences. Ces propriétés sont regroupées dans l'entrée "localMembers" et accessibles
--				   par les fonctions appropriées à partir du module ATME_Core uniquement (en lecture comme en écriture).
--				   L'instance contient un index spécifique accessible en lecture seule y compris à partir de modules
--				   externes, mais pour ces derniers cet index ne sert à rien.
----------------------------------------------------------------------------------------------------------


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Vector3D : gère les vecteurs 3D. Constructeur de classe accessible aux modules externes.
-- Elle donne accès aux calculs de module (module du vecteur AB = distance AB), à l'angle formé avec le nord
-- par AB (Azimuth), à la transformation en vecteur unitaire (vecteur identique de module 1) et au produit
-- scalaire calculé pour les coordonnées horizontales (x et z).
----------------------------------------------------------------------------------------------------------
local C_Vector3D_MT = {
	className = "C_Vector3D",
	mt = {},
	
	keys = {
		"getAzimuth", "getHModule", "getModule", "get", "getX", "getY", "getZ", "toUnitVector", "scalaireH",
	},
}

ATME.C_Vector3D = {
	getClassName = function(self)
		return C_Vector3D_MT.className
	end
}

local function C_Vector3D_create(...)
	local args = {...}
	local x = 0
	local y = 0
	local z = 0
	
	local newVector = {}
	
	setmetatable(newVector, C_Vector3D_MT.mt)
	
	if #args == 3 and type(args[1]) == "number" and type(args[2]) == "number" and type(args[3]) == "number" then
		x = args[1]
		y = args[2]
		z = args[3]
	elseif #args == 1 and ATME.isObjectClass(args[1], C_Vector3D) and type(args[1]:getX()) == "number" and type(args[1]:getY()) == "number" and type(args[1]:getZ()) == "number" then  
		x = args[1]:getX()
		y = args[1]:getY()
		z = args[1]:getZ()
	elseif #args == 1 and type(args[1]) == "table" and type(args[1].x) == "number" and type(args[1].y) == "number" and type(args[1].z) == "number" then
		x = args[1].x
		y = args[1].y
		z = args[1].z
	elseif #args == 2 and ATME.isPoint(args[1]) ~= POINT_BAD and ATME.isPoint(args[2]) ~= POINT_BAD then
		local p1 = ATME.convertToPoint3D(args[1])
		local p2 = ATME.convertToPoint3D(args[2])

		x = p2.x - p1.x
		y = p2.y - p1.y
		z = p2.z - p1.z
	end
	
	local hModule = math.sqrt(x^2 + z^2)
	local gModule = math.sqrt(x^2 + y^2 + z^2)
	
	local angle = 0
	if x ~= 0 and z~= 0 then
		angle = math.deg(math.atan2(z, x))
	end
	
	
	if angle < 0 then
		angle = angle + 360
	end
	
	function newVector.getAzimuth(self)
		return angle
	end
	
	function newVector.getHModule(self)
		return hModule
	end
	
	function newVector.getModule(self)
		return gModule
	end
	
	function newVector.get(self)
		return x, y, z
	end
	
	function newVector.getX(self)
		return x
	end

	function newVector.getY(self)
		return y
	end

	function newVector.getZ(self)
		return z
	end
	
	function newVector.toUnitVector(self)
		local unitVector
		if gModule ~= 0 then
			unitVector = C_Vector3D_create(x/gModule, y/gModule, z/gModule)
		else
			unitVector = C_Vector3D_create()
		end
		
		return unitVector
	end
	
	function newVector.scalaireH(self, v1)
		return v1:getX() * x + v1:getZ() * z
	end
	
	return newVector
end



C_Vector3D_MT.__call = function(t, ...)
	local args = {...}
	if  #args == 0 or (#args == 3 and type(args[1]) == "number" and type(args[2]) == "number" and type(args[3]) == "number") or
		(#args == 1 and ATME.isObjectClass(args[1], ATME.C_Vector3D) and type(args[1]:getX()) == "number" and type(args[1]:getY()) == "number" and type(args[1]:getZ()) == "number") or
		(#args == 1 and type(args[1]) == "table" and type(args[1].x) == "number" and type(args[1].y) == "number" and type(args[1].z) == "number") or
		(#args == 2 and ATME.isPoint(args[1]) ~= POINT_BAD and ATME.isPoint(args[2]) ~= POINT_BAD) then
		return C_Vector3D_create(...)
	else
		errorOutputCore(nil, "Function ATME.C_Vector3D : Bad parameters")
	end
end

setmetatable(ATME.C_Vector3D,C_Vector3D_MT)

C_Vector3D_MT.mt.__metatable = "Private"
C_Vector3D_MT.mt.__index = ATME.C_Vector3D

C_Vector3D_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Vector3D_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_Vector3D : Bad key " .. k)
	end
end

C_Vector3D_MT.mt.__add = function(v1, v2)
	v1x, v1y, v1z = v1:get()
	v2x, v2y, v2z = v2:get()

	return C_Vector3D(v1x + v2x, v1y + v2y, v1z + v2z)
end

C_Vector3D_MT.mt.__sub = function(v1, v2)
	v1x, v1y, v1z = v1:get()
	v2x, v2y, v2z = v2:get()
	
	return C_Vector3D(v1x - v2x, v1y - v2y, v1z - v2z)
end


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Line2D : gère les droites horizontales (2D). Format DCS repère z0x
--
-- Une droite est formée d'un vecteur et d'un point d'origine, référence 0 de la droite pour cette classe.
--
-- Forme Az + Bx + C = 0
-- Vecteur directeur v(xv, yv, zv) => A = xv et B = -zv
-- Point d'origine (xref, yref, zref) => C = -A * zref - B * xref
--
-- Droite perpendiculaire déduite et passant au point d'origine :
-- Vecteur directeur vp(xvp, yvp, zvp) => xvp = B et zvp = A
-- Forme : -Bz + Ax + C = 0 et C = -A * zref + B * xref
--
-- NOTE : l'axe y est non significatif
----------------------------------------------------------------------------------------------------------
local C_Line2D_MT = {
	className = "C_Line2D",
	mt = {},
	
	keys = {
		"get", "getA", "getB", "getC", "getX", "getZ", "isNSAxis", "isWEAxis", "getOrigin", "getPerpendicular",
		"getPointFromOrigin",
	},
}

ATME.C_Line2D = {
	getClassName = function(self)
		return C_Line2D_MT.className
	end
}

local function C_Line2D_create(...)
	local args = {...}
	local a = 0
	local b = 0
	local c = 0
	local origine
	local newLine = {}
	
	setmetatable(newLine, C_Line2D_MT.mt)
	
	if #args == 1 then
		a = args[1]:getA()
		b = args[1]:getB()
		c = args[1]:getC()
		origine = args[1]:getOrigin()
	elseif #args == 2 then
		if ATME.isPoint(args[1]) ~= POINT_BAD and ATME.isPoint(args[2]) ~= POINT_BAD then
			origine = ATME.convertToPoint3D(args[1])
			local p2 = ATME.convertToPoint3D(args[2])

			a = p2.x - origine.x
			b = origine.z - p2.z
			c = - b * origine.x - a * origine.z
		else
			origine = ATME.convertToPoint3D(args[2])

			a = args[1]:getX()
			b = -args[1]:getZ()
			c = - b * origine.x -  a * origine.z
		end
	end
	
	function newLine.get(self)
		return a, b, c
	end
	
	function newLine.getA(self)
		return a
	end

	function newLine.getB(self)
		return b
	end

	function newLine.getC(self)
		return c
	end
	
	function newLine.getZ(self, _x)
		if a ~= 0 then
			return (-b * _x - c) / a
		end
	end
	
	function newLine.getX(self, _z)
		if b ~= 0 then
			return (-a * _z - c) / b
		end
	end
	
	function newLine.isNSAxis(self)
		return b == 0
	end
	
	function newLine.isWEAxis(self)
		return a == 0
	end
	
	function newLine.getOrigin(self)
		local point = {}
		
		point.x = origine.x
		point.y = origine.y
		point.z = origine.z
		
		return point
	end
	
	function newLine.getPointFromOrigin(self, _offset)
		if type(_offset) == "number" then
			local n = math.sqrt(_offset^2/(a^2 + b^2))
			local point = {}
			
			if _offset < 0 then
				n = -n
			end
			
			point.x = origine.x + n * a
			point.y = 0
			point.z = origine.z - n * b
			
			return point
		else
			errorOutputCore(nil, "Function ATME.C_Line2D.getPointFromOrigin : offset type must be number")
		end
	end
		
	function newLine.getPerpendicular(self, _offset)
		if type(_offset) == "number" then
			local v = ATME.C_Vector3D({x = self:getB(), y = 0, z = self:getA()})
			
			local perpendicularLine
			
			if _offset == 0 then
				perpendicularLine = ATME.C_Line2D(v, self:getOrigin())
			else
				perpendicularLine = ATME.C_Line2D(v, self:getPointFromOrigin(_offset))
			end
			
			return perpendicularLine
		else
			errorOutputCore(nil, "Function ATME.C_Line2D.getPerpendicular : offset type must be number")
		end
	end
	
	
	return newLine
end

C_Line2D_MT.__call = function(t, ...)
	local args = {...}
	if (#args == 1 and ATME.isObjectClass(args[1], ATME.C_Line2D))  or
			(#args == 2 and ATME.isPoint(args[1]) ~= POINT_BAD and ATME.isPoint(args[2]) ~= POINT_BAD) or
			(#args == 2 and ATME.isObjectClass(args[1], ATME.C_Vector3D) == true and ATME.isPoint(args[2]) ~= POINT_BAD) then
		return C_Line2D_create(...)
	else
		errorOutputCore(nil, "Function ATME.C_Line2D : Bad arguments")
	end
end

setmetatable(ATME.C_Line2D,C_Line2D_MT)

C_Line2D_MT.mt.__metatable = "Private"
C_Line2D_MT.mt.__index = ATME.C_Line2D

C_Line2D_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Line2D_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_Line2D : Bad key " .. k)
	end
end


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_IndexList : gère une liste indexée par des entiers. Constructeur de classe accessible aux modules 
-- externes. 
-- Elle optimise la place utilisée sans recourir aux fonctions table.insert etc...
-- Le premier indice commence à 1. En cas de suppression, l'entrée indexée est mise à nil et sera réutilisée
-- au prochain ajout dans la liste. Si toutes les entrées sont utilisées, la liste est étendue d'un élément.
-- De plus en cas de suppression, l'index des suivants ne change pas.
----------------------------------------------------------------------------------------------------------

local C_IndexList_MT = {
	className = "C_IndexList",
	mt = {},
	
	keys = {
		"add", "remove", "get", "pairs", "getCount", "removeAll",
	},
}

ATME.C_IndexList = {
	getClassName = function(self)
		return C_IndexList_MT.className
	end
}

local function C_IndexList_create()
	local maxIndex = 1
	local freeIndex = {}
	local list = {}
	local nbItems = 0
	local newList = {}
		
	setmetatable(newList, C_IndexList_MT.mt)
	
	function newList.add(self, _item)
		local index
		if #freeIndex > 0 then
			index = freeIndex[1]
			table.remove(freeIndex, 1)
			maxIndex = maxIndex - 1
		else
			index = maxIndex
		end
		
		list[index] = _item
		
		maxIndex = maxIndex + 1
		nbItems = nbItems + 1
		return index
	end
	
	function newList.remove(self, _id)
		if list[_id] ~= nil then
			list[_id] = nil
			nbItems = nbItems - 1
			
			table.insert(freeIndex, _id)
			
			table.sort(freeIndex,
							function(a, b) 
								return a < b
							end)
		end
	end
	
	function newList.get(self,_id)
		return list[_id]
	end
	
	function newList.getCount(self)
		return nbItems
	end
	
    function newList.pairs(self)
		local i = 0
		return function()
			repeat
				i = i + 1
			until list[i] ~= nil or i == maxIndex
			
			if i < maxIndex then
				return i, list[i]
			end
		end
    end
	
	function newList.removeAll(self)
		freeIndex = {}
		list = {}
		maxIndex = 1
		nbItems = 0
	end
	
	return newList
end

C_IndexList_MT.__call = function(t)
	return C_IndexList_create()
end

setmetatable(ATME.C_IndexList,C_IndexList_MT)


C_IndexList_MT.mt.__metatable = "Private"
C_IndexList_MT.mt.__index = ATME.C_IndexList
C_IndexList_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_IndexList_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_IndexList : Bad key " .. k)
	end
end

----------------------------------------------------------------------------------------------------------
-- Variables globales utilisant les C_Index_List : à mettre ici sinon erreur.
----------------------------------------------------------------------------------------------------------
activatedRadioTransmissions = ATME.C_IndexList()
local activatedSmokes = ATME.C_IndexList()
local activatedFlares = ATME.C_IndexList()
local activatedSounds = ATME.C_IndexList()
local groupsToBoard = ATME.C_IndexList()
local signals = ATME.C_IndexList()

----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Airbase : gère les bases aériennes (aéroports ou FARP). Pas de contructeur accessible aux modules externes.
-- Trois constructeurs privés, un générique de base, un pour les bases de la cartes (l'index est un nombre) et un 
-- pour les bases créées dans la mission (les Airfields et les FARP dont l'index est un string et correspond au unitId).
-- Cette classe donne la liste des airbases de la map (indexés par un nombre) et les FARP/Airfields créés 
-- dans la mission, indexés par une chaine de caractère dans le mission. Cet index est leur groupId ou unitId.
-- Deux listes globales sont créés et initialisées :
--			-> airbasesByID pour les bases de la carte
--			-> helipadsAirFieldsByID pour les bases créées dans la mission
----------------------------------------------------------------------------------------------------------

local C_Airbase_MT = {
	className = "C_Airbase",
	mt = {},
	
	keys = {
		"getName",
	},
}

ATME.C_Airbase = {
	getClassName = function(self)
		return C_Airbase_MT.className
	end
}

local function C_Airbase_create_basic(_name, _id, _category, _airbases)
		local id = _id
		local name = _name
		local category = _category
		local airbases = _airbases
		local newAirbase = {}
		
		setmetatable(newAirbase, C_Airbase_MT.mt)
	
		airbases[id] = newAirbase
		
		function newAirbase.getName(self)
			return name
		end
		
		return newAirbase
end

local function C_Airbase_create(_name, _id)
	return C_Airbase_create_basic(_name, _id, "AIRBASE", airbasesByID)
end

local function C_Airbase_create_helipad_airField(_name, _id)
	return C_Airbase_create_basic(_name, _id, "HELIPAD_AIRFIELD", helipadsAirFieldsByID)
end

setmetatable(ATME.C_Airbase,C_Airbase_MT)

C_Airbase_MT.mt.__metatable = "Private"
C_Airbase_MT.mt.__index = ATME.C_Airbase
C_Airbase_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Airbase_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_Airbase : Bad key " .. k)
	end
end


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_F10Menu : gère les menus accessibles par F10. Constructeur accessible aux modules externes 
-- Cette classe est complexe et gère :
--			- Les entrées d'un menu (menu de base root ou sous menu). Un entrée engendre une action par l'appel
--			  d'une fonction.
--			- Les sous-menus rattachés :
--					-> au menu de base root (accessible directement par F10)
--					-> à un menu, lui même sous-menu du menu de base rout ou d'un autre menu.
--			- L'affichage dans l'ordre choisi (alpha par défaut) des sous-menus du menu courant et de ses entrées
--					-> sous menus d'abord
--					-> entrées ensuite
--			- La gestion des entrées par une identification de type groupId pour une gestion en groupe :
--					-> lors de l'affichage dans l'ordre par groupId puis alpha si groupId identique
--					-> pour la suppression de plusieurs entrées  simultanément.
--					-> le groupId d'une entrée s'applique à toutes les entrées d'un menu donné, y compris 
--					   pour les entrées de ses sous-menus
--			- Un menu vide n'est plus affiché. Un menu est vide si lui-même et ses sous-menus ne contiennent
--			  aucune entrée
----------------------------------------------------------------------------------------------------------

local C_F10Menu_MT = {
	className = "C_F10Menu",
	mt = {},
	
	keys = {
		"setOrder", "getParent", "isRoot", "getGroupId", "getLabel", "getIndex", "append", "removeAll", "remove",
	},
		
	localMembers = ATME.C_IndexList(),
	
	add = function(self, _itemCommandsList, _F10SubMenusList)
		local item = {
			itemCommandsList = _itemCommandsList,
			F10SubMenusList = _F10SubMenusList,
			isActive = false,
			path = nil,
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:get(_id)._itemCommandsList = nil
		self.localMembers:get(_id)._F10SubMenusList = nil
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

local C_F10Menu_flagOrder = false

ATME.C_F10Menu = {
	getClassName = function(self)
		return C_F10Menu_MT.className
	end,
}

local function C_F10Menu_initialize(_player, _label, _parent, _isRoot)
	
	local foundSubMenu = 0
	local foundItem = 0
	
	if _isRoot == false then
		local currentMenu
		
		if _parent ~= nil then
			currentMenu = _parent
		else
			currentMenu = _player:getF10MenuRoot()
		end
		
		local  currentLocalMembers= C_F10Menu_MT:get(currentMenu:getIndex())
		
		for _i, _item in ipairs(currentLocalMembers.F10SubMenusList) do
			if _item:getLabel() == _label then
				foundSubMenu = _i
			end
		end
		
		for _i, _item in ipairs(currentLocalMembers.itemCommandsList) do
			if _item.label == _label then
				foundItem = _i
			end
		end
	end
	
	if foundItem == 0 and foundSubMenu == 0 then
		local newF10SubMenu = {}
		
		setmetatable(newF10SubMenu, C_F10Menu_MT.mt)
			
		local groupId = _player:getGroupId()
		local itemCommandsList = {}
		local F10SubMenusList = {}
		
		local index = C_F10Menu_MT:add(itemCommandsList, F10SubMenusList)
		
		local isRoot = _isRoot
		local label = _label
		local parent
		
		function newF10SubMenu.getParent(self)
			return parent
		end
		
		function newF10SubMenu.isRoot(self)
			return isRoot
		end
		
		function newF10SubMenu.getGroupId(self)
			return groupId
		end
		
		function newF10SubMenu.getLabel(self)
			return label
		end
		
		function newF10SubMenu.getIndex(self)
			return index
		end
		
		local function C_F10Menu_deleteAllCommands(_itemF10Menu)
			if _itemF10Menu:getLabel() == nil then
				outputCore(nil, "Start Suppression -> Root", 3)
			else
				outputCore(nil, "Start Suppression -> " .. _itemF10Menu:getLabel(), 3)
			end
			
			local localMembers = C_F10Menu_MT:get(_itemF10Menu:getIndex())
			while #localMembers.itemCommandsList > 0 do
				missionCommands.removeItemForGroup(_itemF10Menu:getGroupId(), localMembers.itemCommandsList[1].itemF10MenuCommandId)
				table.remove(localMembers.itemCommandsList, 1)
			end
		  
			local i = 1
			while i <= #localMembers.F10SubMenusList do
				C_F10Menu_deleteAllCommands(localMembers.F10SubMenusList[i])
				i = i + 1
			end
		  
			if localMembers.isActive == true then
				missionCommands.removeItemForGroup(_itemF10Menu:getGroupId(), localMembers.path)
				localMembers.isActive = false
			end

			if _itemF10Menu:getLabel() == nil then
				outputCore(nil, "End Suppression -> Root", 3)
			else
				outputCore(nil, "End Suppression -> " .. _itemF10Menu:getLabel(), 3)
			end
		end

		local function C_F10Menu_deleteCommands(_itemF10Menu, _groupId)
			if _itemF10Menu:getLabel() == nil then
				outputCore(nil, "Start Suppression -> Root", 3)
			else
				outputCore(nil, "Start Suppression -> " .. _itemF10Menu:getLabel(), 3)
			end
		  
			local localMembers = C_F10Menu_MT:get(_itemF10Menu:getIndex())
			local i = 1
			while i <= #localMembers.itemCommandsList do
				if localMembers.itemCommandsList[i].groupId == _groupId then    
					missionCommands.removeItemForGroup(_itemF10Menu:getGroupId(), localMembers.itemCommandsList[i].itemF10MenuCommandId)
					table.remove(localMembers.itemCommandsList, i)
				else
					i = i + 1
				end
			end
			
			local ok = true
			i = 1
			while i <= #localMembers.F10SubMenusList do
				if C_F10Menu_deleteCommands(localMembers.F10SubMenusList[i], _groupId) == true then
					ok = false
				end
				i = i + 1
			end

			if #localMembers.itemCommandsList == 0 and ok == true then
				if localMembers.isActive == true then
					missionCommands.removeItemForGroup(_itemF10Menu:getGroupId(), localMembers.path)
					localMembers.isActive = false
				end
			end
			
			if _itemF10Menu:getLabel() == nil then
				outputCore(nil, "End Suppression -> Root", 3)
			else
				outputCore(nil, "End Suppression -> " .. _itemF10Menu:getLabel(), 3)
			end
			
			return localMembers.isActive
		end
		
		local function C_F10Menu_updateAll(_itemF10Menu)
			local function refresh(_itemF10Menu)
				local localMembers = C_F10Menu_MT:get(_itemF10Menu:getIndex())
				local i = 1
				while i <= #localMembers.F10SubMenusList do
					local childPrivateMembers = C_F10Menu_MT:get(localMembers.F10SubMenusList[i]:getIndex())
					if childPrivateMembers.isActive == true then
						if _itemF10Menu:isRoot() == true then
							childPrivateMembers.path = missionCommands.addSubMenuForGroup(localMembers.F10SubMenusList[i]:getGroupId(), localMembers.F10SubMenusList[i]:getLabel(), nil)
						else
							childPrivateMembers.path = missionCommands.addSubMenuForGroup(localMembers.F10SubMenusList[i]:getGroupId(), localMembers.F10SubMenusList[i]:getLabel(), localMembers.path)
						end
						refresh(localMembers.F10SubMenusList[i])
					end
					i = i + 1
				end
				
				i = 1
				while i <= #localMembers.itemCommandsList do
					localMembers.itemCommandsList[i].itemF10MenuCommandId = missionCommands.addCommandForGroup(_itemF10Menu:getGroupId(), 
																						localMembers.itemCommandsList[i].label, localMembers.path, localMembers.itemCommandsList[i].handler , localMembers.itemCommandsList[i].args)
					i = i + 1
				end
			end
		
			refresh(_itemF10Menu)
		end
		
		local function C_F10Menu_deleteAllDCSF10Menu(_itemF10Menu)
			local function deleteDCSSubMenu(_itemF10Menu)
				local localMembersCurrent = C_F10Menu_MT:get(_itemF10Menu:getIndex())
				local i = 1
				while i <= #localMembersCurrent.F10SubMenusList do
					local childPrivateMembers = C_F10Menu_MT:get(localMembersCurrent.F10SubMenusList[i]:getIndex())
					if childPrivateMembers.isActive == true then
						deleteDCSSubMenu(localMembersCurrent.F10SubMenusList[i])
						outputCore(nil, "Suppression sous menu ".. localMembersCurrent.F10SubMenusList[i]:getLabel(), 3)
						missionCommands.removeItemForGroup(localMembersCurrent.F10SubMenusList[i]:getGroupId(), childPrivateMembers.path)
					end
					i = i + 1
				end
				
				i = 1
				
				while i <= #localMembersCurrent.itemCommandsList do
					outputCore(nil, " -> Suppression commande ".. localMembersCurrent.itemCommandsList[i].label,3)
					missionCommands.removeItemForGroup(_itemF10Menu:getGroupId(), localMembersCurrent.itemCommandsList[i].itemF10MenuCommandId)
					i = i + 1
				end
			end
			
			-- On remonte à root pour balayer l'arborescence
			local root = _itemF10Menu
			while root:isRoot() == false do
				root = root:getParent()
			end
					
			deleteDCSSubMenu(root)
			
			return root
		end
		
		function newF10SubMenu.append(self, _groupId, _label, _handler, _args)
			if type(_groupId) == "number" and type(_label) == "string" and type(_handler) == "function" then
				if _groupId >= 1 then
					outputCore(nil, "Start append -> " .. _label, 3)
					local found = false
					local localMembers = C_F10Menu_MT:get(self:getIndex())
					for _, _item in ipairs(localMembers.itemCommandsList) do
						if _item.label == _label then
							found = true
						end
					end
				
					if found == false then
						local root = C_F10Menu_deleteAllDCSF10Menu(self)
						
						if #localMembers.itemCommandsList == 0 and isRoot == false then
							if localMembers.isActive == false and parent:isRoot() == true then
								localMembers.isActive = true
							else
								local current = self
								local pathToRoot = {}
								while current:isRoot() == false do
									localMembersCurrent = C_F10Menu_MT:get(current:getIndex())
									if localMembersCurrent.isActive == false then
										table.insert(pathToRoot, 1, current)
									end
									current = current:getParent()
								end
								
								for _, _current in ipairs(pathToRoot) do
									localMembersCurrent = C_F10Menu_MT:get(_current:getIndex())
									localMembersCurrent.isActive = true
								end
							end
						end
						
						local itemRadio = {
								groupId = math.floor(_groupId),
								label = _label,
								handler = _handler,
								args = _args,
						}
						
						table.insert(localMembers.itemCommandsList,itemRadio)
						if C_F10Menu_flagOrder == true then
							table.sort(localMembers.itemCommandsList, 
										function(a, b) 
											if a.groupId == b.groupId then
												return a.label < b.label
											else
												return a.groupId < b.groupId
											end
										end)
						else
							table.sort(localMembers.itemCommandsList, 
										function(a, b) 
											return a.label < b.label
										end)
						end
						
						C_F10Menu_updateAll(root)
					else
						errorOutputCore(nil, "Function ATME.C_F10Menu.append : Label " .. _label .. " already used in menu " .. self:getLabel())
					end
				else
					errorOutputCore(nil, "Function ATME.C_F10Menu.append : groupId must be >= 1")
				end
			else
				if type(_groupId) ~= "number" then
					errorOutputCore(nil, "Function ATME.C_F10Menu.append : groupId type must be number")
				elseif type(_label) ~= "string" then
					errorOutputCore(nil, "Function ATME.C_F10Menu.append : label type must be string")
				elseif type(_handler) ~= "function" then
					errorOutputCore(nil, "Function ATME.C_F10Menu.append : handler type must be function")
				end
			end
		end
		
		function newF10SubMenu.removeAll(self)
			C_F10Menu_deleteAllCommands(self) 
		end
		
		function newF10SubMenu.remove(self, _groupId)
			if type(_groupId) == "number" then
				if _groupId >= 1 then
					C_F10Menu_deleteCommands(self, math.floor(_groupId)) 
				else
					errorOutputCore(nil, "Function ATME.C_F10Menu.remove : groupId must be >= 1")
				end
			else
				errorOutputCore(nil, "Function ATME.C_F10Menu.remove : groupId type must be number")
			end
		end
		

		if isRoot == false then
			if _parent ~= nil then
				parent = _parent
			else
				parent = _player:getF10MenuRoot()
			end
			
			local localMembersParent = C_F10Menu_MT:get(parent:getIndex())
			table.insert(localMembersParent.F10SubMenusList, newF10SubMenu)
			
			table.sort(localMembersParent.F10SubMenusList, 
						function(a, b) 
							return a:getLabel() < b:getLabel()
						end)
			
			if parent == _player:getF10MenuRoot() then
				outputCore(nil, "Création subMenu " .. label .. " à partir de Root (" .. #localMembersParent.F10SubMenusList .. ")", 3)
			else
				outputCore(nil, "Création subMenu " .. label .. " à partir de " .. _parent:getLabel(), 3)
			end	
		end
		
		return false, newF10SubMenu
	end
	
	-- Erreur doublon de label
	return true
end

local function C_F10Menu_createRoot(_player)
	if ATME.isObjectClass(_player, ATME.C_Player) == true  then
		-- Menu root aucun risque d'erreur
		
		local err, menu = C_F10Menu_initialize(_player, nil, nil, true)
		
		return menu
	else
		errorOutputCore(nil, "Function C_F10Menu_createRoot : player type must be ATME.C_Player")
	end
end

local function C_F10Menu_create(_player, _label, _parent)
	if ATME.isObjectClass(_player, ATME.C_Player) == true  and type(_label) == "string" and (ATME.isObjectClass(_parent, ATME.C_F10Menu) == true or _parent == nil) then
		local err, menu = C_F10Menu_initialize(_player, _label, _parent, false)
		if err == false then
			return menu
		else
			errorOutputCore(nil, "Function ATME.C_F10Menu : Label " .. _label .. " already used in menu " .. _parent:getLabel())
		end 
	else
		if ATME.isObjectClass(_player, ATME.C_Player) == false then
			errorOutputCore(nil, "Function ATME.C_F10Menu : player type must be ATME.C_Player")
		elseif type(_label) ~= "string" then
			errorOutputCore(nil, "Function ATME.C_F10Menu : label type must be string")
		elseif ATME.isObjectClass(_parent, ATME.C_F10Menu) == false and _parent ~= nil then
			errorOutputCore(nil, "Function ATME.C_F10Menu : parent type must be ATME.C_F10Menu or nil")
		end
	end
end

C_F10Menu_MT.__call = function(t, _player, _label, _parent)
	return C_F10Menu_create(_player, _label, _parent)
end

setmetatable(ATME.C_F10Menu,C_F10Menu_MT)

C_F10Menu_MT.mt.__metatable = "Private"
C_F10Menu_MT.mt.__index = ATME.C_F10Menu
C_F10Menu_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_F10Menu_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_F10Menu : Bad key " .. k)
	end
end

----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_UserTrigger : gère les triggers utilisateur. Pas de contructeur accessible aux modules externes.
-- En effet, la création d'un trigger utilisateur passe par deux méthodes de la classe C_Module.
-- Il existe deux type de trigger utilisateur :
--		- Les triggers absolus, déclenchant un event ATME de type user à un moment précis sans autre critère.
--		- Les triggers relatifs à un flag, déclenchant un event ATME de type user à un moment précis conditionné par 
--		  l'activation d'un flag.
-- La condition de temps est donné par un string au format particulier :
--		- "X..X" pour un temps en secondes depuis le début de mission (ex : "10" ou "100" pour 10s ou 100s)
--		- "+X..X" pour un temps en seconde depuis la création du trigger (ex : "+10" pour 10s après la création)
--		- "HH:MM" ou "MM:MM:SS" pour remplacer une expression en secondes par une expression en m/s ou h/m/s
--		- "X..X Y..Y" avec ou sans "+" pour "X..X " pour définir deux bornes debut et fin avec les mêmes conditions 
--        que précédemment. Un plus devant "Y..Y" engendre un ajout de "Y..Y" secondes à partir du début.
----------------------------------------------------------------------------------------------------------

local C_UserTrigger_MT = {
	className = "C_UserTrigger",
	mt = {},
	
	keys = {
		"getFlag", "getName", "getTimeStart", "getTimeEnd", "isArmed", "isActivated", "getIndex",
	},
	
	localMembers = ATME.C_IndexList(),
	
	add = function(self, _armed, _timeStart, _timeEnd)
		local item = {
			armed = _armed,
			activated = false,
			timeStart = _timeStart,
			timeEnd = _timeEnd,
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

ATME.C_UserTrigger = {
	getClassName = function(self)
		return C_UserTrigger_MT.className
	end,
}

local function C_UserTrigger_create(_name, _moduleName, _flagNumber, _condition)
	local name = _name
	local moduleName = _moduleName
	local flag = -1 
	
	local function getTimeConditionInterval(_name, _isRelative)
		local pos = 1
		local add = false
		local param = 0
		local values = {}
		local adds = {}
		local h, m, s
		local tokenError = false

		while pos <= _name:len() and tokenError == false do
			h, m, s = string.match(_name,"^(%d%d):(%d%d):(%d%d)", pos)
	  
			if h == nil then
				s = string.match(_name,"^%d+", pos)
				if s ~= nil then
					param = param + 1
					pos = pos + s:len()
					values[param] =  tonumber(s)
					adds[param] = add
					add = false
				else
					s = string.match(_name,"^%s+", pos)
					if s ~= nil then
						pos = pos + s:len()
					else
						s = string.match(_name,"^%+%s*", pos)
						if s ~= nil then
							add = true
							pos = pos + s:len()
						else
							tokenError = true
						end
					end
				end
			else
				param = param + 1
				pos = pos + 8
				if tonumber(m) > 59 or tonumber(s) > 59 then
					tokenError = true
				end
				values[param] =  tonumber(h) * 3600 + tonumber(m)*60 + tonumber(s)
				adds[param] = add
				add = false
			end
		end 
		
		local timeStart
		local timeEnd
		
		if tokenError == false then
			if param > 2 then
				tokenError = true
			else
				if adds[1] == true and _isRelative == false then
					timeStart = values[1] + math.floor(ATME.getTime())
				else
					timeStart = values[1]
				end
				if adds[2] == true then
					timeEnd = values[2] + timeStart
				elseif values[2] ~= nil then
					if values[2] > values[1] then
						timeEnd = values[2]
					else
						tokenError = true
					end
				elseif values[1] ~= nil then
					timeEnd = timeStart
				else
					tokenError = true
				end
			end
		end
	  
		return tokenError, timeStart, timeEnd
	end
	
	local isRelative = false
	if _flagNumber ~= nil then
		flag = math.floor(_flagNumber)
		isRelative = true
	end
	
	local index
	local err, timeStart, timeEnd = getTimeConditionInterval(_condition, isRelative)
	
	if err == false then
		local newTrigger = {}

		setmetatable(newTrigger, C_UserTrigger_MT.mt)
		
		function newTrigger.getFlag(self)
			return flag
		end
		
		function newTrigger.getName(self)
			return name
		end
		
		function newTrigger.getTimeStart(self)
			local localMembers = C_UserTrigger_MT:get(index)
			
			return localMembers.timeStart
		end
		
		function newTrigger.getTimeEnd(self)
			local localMembers = C_UserTrigger_MT:get(index)
			
			return localMembers.timeEnd
		end
		
		function newTrigger.isArmed(self)
			local localMembers = C_UserTrigger_MT:get(index)
			
			return localMembers.armed
		end
		
		function newTrigger.isActivated(self)
			local localMembers = C_UserTrigger_MT:get(index)
			
			return localMembers.activated
		end
		
		function newTrigger.getIndex(self)
			return index
		end
		
		index = C_UserTrigger_MT:add(isRelative == false, timeStart, timeEnd)
		
		
				
		if isRelative == true then
			outputCore(nil,"Module " .. moduleName .. " - Trigger " .. newTrigger:getName() .. " créé et non armé, armement sur flag" , 2)
		else
			outputCore(nil,"Module " .. moduleName .. " - Trigger " .. newTrigger:getName() .. " créé et armé, absolu (timeStart = ".. timeStart .. "/ timeEnd = "..timeEnd..")" , 2)
		end
		
		return newTrigger
	end
end


setmetatable(ATME.C_UserTrigger,C_UserTrigger_MT)

C_UserTrigger_MT.mt.__metatable = "Private"
C_UserTrigger_MT.mt.__index = ATME.C_UserTrigger
C_UserTrigger_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_UserTrigger_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_UserTrigger : Bad key " .. k)
	end
end


----------------------------------------------------------------------------------------------------------
-- Classe interne C_EventsMgr : La classe C_EventsMgr permet la création d'instance gestionnaires d'events.
--
-- Gestionnaire des événements core et trigger
-- 		- Permet de savoir s'il s'agit d'un event core ou d'un event lié à un user trigger
-- 		- Ajoute les méthodes de récupération des données des événements core
-- 		  et les méthodes pour les events associés aux user triggers.
---------------------------------------------------------------------------------------------------------- 
local C_EventsMgr_MT = {
	className = "C_EventsMgr",
	mt = {},
	
	keys = {
		"getIndex", "pairs", "isCoreEvent", "isUserTriggerEvent", "isUserTriggerEventToggle", "getCoreEventType", 
		"getCoreEventDatas",
	},
	
	localMembers = ATME.C_IndexList(),
	
	add = function(self)
		local item = {
			list = {},
			nbCoreEvents = 0,
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

local C_EventsMgr = {
	getClassName = function(self)
		return C_EventsMgr_MT.className
	end,
}

setmetatable(C_EventsMgr, C_EventsMgr_MT)

C_EventsMgr_MT.mt.__metatable = "Private"
C_EventsMgr_MT.mt.__index = C_EventsMgr
C_EventsMgr_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_EventsMgr_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function C_EventsMgr : Bad key " .. k)
	end
end

local function C_EventsMgr_Create()
	local newEventsMgr = {}
	
	setmetatable(newEventsMgr, C_EventsMgr_MT.mt)
	local index = C_EventsMgr_MT:add()
	
	local localMembers = C_EventsMgr_MT:get(index)
	
	function newEventsMgr.getIndex()
		return index
	end
	
	function newEventsMgr.pairs(self)
	  return pairs(localMembers.list)
	end
	
	function newEventsMgr.isCoreEvent(self, _id)
		if type(_id) == "number"  then
			local id = math.floor(_id)
			return localMembers.list[id] ~= nil
		else
			return false
		end
	end
	
	function newEventsMgr.isUserTriggerEvent(self, _id)
		return type(_id) == "string" and localMembers.list[_id] ~= nil
	end
	
	function newEventsMgr.isUserTriggerEventToggle(self, _id)
		if type(_id) == "string" and localMembers.list[_id] ~= nil then
			return localMembers.list[_id] == true
		end
	end
	
	function newEventsMgr.getCoreEventType(self, _id)
		if type(_id) == "number" then
			local id = math.floor(_id)
			return localMembers.list[id].type
		end
	end
	
	function newEventsMgr.getCoreEventDatas(self, _id)
		if type(_id) == "number" then
			local id = math.floor(_id)
			local datas = {}
			if localMembers.list[id].type == "RACE_PLAYER_TOO_HIGH" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.penalty = localMembers.list[id].datas.penalty
			elseif localMembers.list[id].type == "RACE_START" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
			elseif localMembers.list[id].type == "RACE_DOOR_MISSED" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.doorNumber = localMembers.list[id].datas.doorNumber
			elseif localMembers.list[id].type == "RACE_TURN_TIME" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.playTime = localMembers.list[id].datas.playTime
				datas.totalPlayTime = localMembers.list[id].datas.totalPlayTime
				datas.penalty = localMembers.list[id].datas.penalty
				datas.turnNumber = localMembers.list[id].datas.turnNumber
			elseif localMembers.list[id].type == "RACE_PLAYER_BEST_TURN_TIME" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.playTime = localMembers.list[id].datas.playTime
				datas.turnNumber = localMembers.list[id].datas.turnNumber
			elseif localMembers.list[id].type == "RACE_BEST_TURN_TIME" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.playTime = localMembers.list[id].datas.playTime
				datas.turnNumber = localMembers.list[id].datas.turnNumber
			elseif localMembers.list[id].type == "RACE_LAP_TIME" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.playTime = localMembers.list[id].datas.playTime
				datas.totalPlayTime = localMembers.list[id].datas.totalPlayTime
				datas.penalty = localMembers.list[id].datas.penalty
				datas.turnNumber = localMembers.list[id].datas.turnNumber
				datas.doorNumber = localMembers.list[id].datas.doorNumber
			elseif localMembers.list[id].type == "RACE_BEST_LAP_TIME" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.playTime = localMembers.list[id].datas.playTime
				datas.turnNumber = localMembers.list[id].datas.turnNumber
				datas.doorNumber = localMembers.list[id].datas.doorNumber
			elseif localMembers.list[id].type == "RACE_PLAYER_BEST_FINAL_TIME" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.totalPlayTime = localMembers.list[id].datas.totalPlayTime
			elseif localMembers.list[id].type == "RACE_BEST_FINAL_TIME" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.totalPlayTime = localMembers.list[id].datas.totalPlayTime
			elseif localMembers.list[id].type == "RACE_FINAL_TIME" then
				datas.race = localMembers.list[id].datas.race
				datas.unit = localMembers.list[id].datas.unit
				datas.totalPlayTime = localMembers.list[id].datas.totalPlayTime
				datas.penalty = localMembers.list[id].datas.penalty
			elseif localMembers.list[id].type == "TRANSPORT_END_OF_BOARDING" then
				datas.unit = localMembers.list[id].datas.unit
				datas.index = localMembers.list[id].datas.index
				datas.onBoardGroupName = localMembers.list[id].datas.onBoardGroupName
				datas.nbOnBoardUnits = localMembers.list[id].datas.nbOnBoardUnits
				datas.nbDeadUnits = localMembers.list[id].datas.nbDeadUnits
			elseif localMembers.list[id].type == "TRANSPORT_END_OF_DISEMBARK" then
				datas.unit = localMembers.list[id].datas.unit
				datas.index = localMembers.list[id].datas.index
				datas.linkedGroup = localMembers.list[id].datas.linkedGroup
				datas.nbOnBoardUnits = localMembers.list[id].datas.nbOnBoardUnits
				datas.nbDeadUnits = localMembers.list[id].datas.nbDeadUnits
			elseif localMembers.list[id].type == "SIGNAL_UNIT_IN_ZONE" then
				datas.unit = localMembers.list[id].datas.unit
				datas.linkedGroup = localMembers.list[id].datas.linkedGroup
			elseif localMembers.list[id].type == "SIGNAL_UNIT_OUT_OF_ZONE" then
				datas.unit = localMembers.list[id].datas.unit
				datas.linkedGroup = localMembers.list[id].datas.linkedGroup
			end
			
			return datas
		end
	end
	
	return newEventsMgr
end

-- Définition du deux events mgr : la bascule se fait dans la fonction main timer
-- Ceci permet si des événements sont créés dans cette fonction, de les traiter
-- au coup suivant. Dans main timer, activeEventsMgr passe de 1 à 0 et vice versa.

local eventsMgr = {
	[0] = C_EventsMgr_Create(),
	[1] = C_EventsMgr_Create(),
}

local activeEventsMgr = 0

local function C_EventsMgr_addCoreEvent(_item)
	local localMembers = C_EventsMgr_MT:get(eventsMgr[activeEventsMgr]:getIndex())
	localMembers.nbCoreEvents = localMembers.nbCoreEvents + 1
	localMembers.list[localMembers.nbCoreEvents] = _item
end

local function C_EventsMgr_removeAll(_id)
	local localMembers = C_EventsMgr_MT:get(eventsMgr[_id]:getIndex())
	for _id, _ in pairs(localMembers.list) do
		localMembers.list[_id] = nil
	end
	
	localMembers.nbCoreEvents = 0
end

local function C_EventsMgr_addTriggerUserEvents(_id, _triggerUserName, _value)
	local localMembers = C_EventsMgr_MT:get(eventsMgr[_id]:getIndex())
	localMembers.list[_triggerUserName] = _value
end

local function C_EventsMgr_removeAllTriggerUserEvents(_id)
	local localMembers = C_EventsMgr_MT:get(eventsMgr[_id]:getIndex())
	for _id, _ in pairs(localMembers.list) do
		if type(_id) == "string" then
			localMembers.list[_id] = nil
		end
	end
end

----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Module : gère les modules externes. Contructeur accessible aux modules externes.
-- Tout module externe doit créer une instance de cette classe afin de pouvoir activer les handlers définis
-- dans le module externe. Un handler à nil, donc non défini, ne sera pas activé par ATME_Core.
-- Cette classe offre des fonction liées au event ATME et à leurs données, permet la création des triggers
-- utilisateurs dont les identifiants sont spécifiques à un module et ajoute des fonction de debug.
----------------------------------------------------------------------------------------------------------

-- C_Module_MT prédéclarée avant en local
C_Module_MT = {
	className = "C_Module",
	mt = {},
	
	keys = {
		"errorOutput", "output", "createAbsoluteUserTrigger", "createFlagRelativeUserTrigger", "userTriggerExists", "getUserTriggerByName", 
		"removeUserTrigger", "getName", "getIndex", "isDebugOn",
	},
	
	localMembers = ATME.C_IndexList(),
	
	add = function(self, _name, _newHandlers)
		local item = {
			name = _name,
			userTriggerEvents = {},
			handlers = _newHandlers,
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:get(_id).userTriggerEvents = nil
		self.localMembers:get(_id).handlers = nil
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
	
	pairs = function(self)
		return self.localMembers:pairs()
	end
}

ATME.C_Module = {
	getClassName = function(self)
		return C_Module_MT.className
	end,
}

local function C_Module_create(_name, _handlers, _debugSwitch)		   
   local newHandlers = {
		onCreatePlayerHandler = _handlers.onCreatePlayerHandler,
		onDeletePlayerHandler = _handlers.onDeletePlayerHandler,
		onUpdatePlayerHandler = _handlers.onUpdatePlayerHandler,
		onTakeoffPlayerHandler = _handlers.onTakeoffPlayerHandler,
		onLandingPlayerHandler = _handlers.onLandingPlayerHandler,
		onStartEnginePlayerHandler = _handlers.onStartEnginePlayerHandler,
		onStopEnginePlayerHandler = _handlers.onStopEnginePlayerHandler,
		
		onCreateAIUnitHandler = _handlers.onCreateAIUnitHandler,
		onDeleteAIUnitHandler = _handlers.onDeleteAIUnitHandler,
		onDisableAIUnitHandler = _handlers.onDisableAIUnitHandler,
		onTakeoffAIUnitHandler = _handlers.onTakeoffAIUnitHandler,
		onLandingAIUnitHandler = _handlers.onLandingAIUnitHandler,
		onStartEngineAIUnitHandler = _handlers.onStartEngineAIUnitHandler,
		onStopEngineAIUnitHandler = _handlers.onStopEngineAIUnitHandler,
		
		onCreateGroupHandler = _handlers.onCreateGroupHandler,
		onDeleteGroupHandler = _handlers.onDeleteGroupHandler,
		onDisableGroupHandler = _handlers.onDisableGroupHandler,
		
		onCreateStaticObjectHandler = _handlers.onCreateStaticObjectHandler,
		onDeleteStaticObjectHandler = _handlers.onDeleteStaticObjectHandler,
		
		onTimerHandler = _handlers.onTimerHandler,
		onModuleStartHandler = _handlers.onModuleStartHandler,
	}
	
	local newModule = {}
	setmetatable(newModule, C_Module_MT.mt)
	
	local debugSwitch = _debugSwitch
	local index = C_Module_MT:add(_name, newHandlers)
	
	if ATME.modules[_name] == nil then
		ATME.modules[_name] = {}
	end
	
	function newModule.getName(self)
		local localMembers = C_Module_MT:get(index)
			
		return localMembers.name
	end
	
	function newModule.getIndex(self)
		return index
	end
	
	function newModule.isDebugOn(self)
		return debugSwitch
	end
	
	function newModule.output(self, _text, _level)
		if type(_text) == "string" and type(_level) == "number" then
			if _level >= 0 then
				outputCore(self, _text, _level)
			else
				errorOutputCore(nil, "Function ATME.C_Module : level must be >= 0")
			end
		else
			if type(_text) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Module.output : text type must be string")
			elseif type(_level) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Module.output : level type must be number")
			end
		end
	end
	
	function newModule.errorOutput(self, _text)
		if type(_text) == "string" then
			errorOutputCore(self, _text)
		else
			errorOutputCore(nil, "Function ATME.C_Module.errorOutput : text type must be string")
		end
	end
	
	function newModule.createAbsoluteUserTrigger(self, _name, _condition)
		if type(_name) == "string" and type(_condition) == "string" then
			local localMembers = C_Module_MT:get(self:getIndex())
			local userTriggerEvents = localMembers.userTriggerEvents
			if userTriggerEvents[_name] == nil then
				userTriggerEvents[_name] = C_UserTrigger_create(_name, self:getName(), nil, _condition)
				if userTriggerEvents[_name] == nil then
					errorOutputCore(nil, "Function ATME.C_Module.createAbsoluteUserTrigger : condition format is bad")
				end
				return userTriggerEvents[_name]
			else
				errorOutputCore(nil, "Function ATME.C_Module.createAbsoluteUserTrigger : trigger " .. _name .. " already exists")
			end
		else
			if type(_name) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Module.createAbsoluteUserTrigger : name type must be string")
			elseif type(_condition) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Module.createAbsoluteUserTrigger : condition type must be string (format)")
			end
		end
	end
		
	function newModule.createFlagRelativeUserTrigger(self, _name, _flagNumber, _condition)
		if type(_name) == "string" and type(_condition) == "string" and type(_flagNumber) == "number" then
			local localMembers = C_Module_MT:get(self:getIndex())
			local userTriggerEvents = localMembers.userTriggerEvents
					
			if userTriggerEvents[_name] == nil then
				local flag = math.floor(_flagNumber)
				
				if flag > 0 then
					userTriggerEvents[_name] = C_UserTrigger_create(_name, self:getName(), flag, _condition)
					if userTriggerEvents[_name] == nil then
						errorOutputCore(nil, "Function ATME.C_Module.createFlagRelativeTrigger : condition format is bad")
					end
					return userTriggerEvents[_name]
				else
					errorOutputCore(nil, "Function ATME.C_Module.createFlagRelativeTrigger : flagNumber must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Module.createFlagRelativeTrigger : Trigger " .. _name .. " already exists")
			end
		else
			if type(_name) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Module.createFlagRelativeUserTrigger : name type must be string")
			elseif type(_flagNumber) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Module.createFlagRelativeUserTrigger : flagNumber type must be number")
			elseif type(_condition) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Module.createFlagRelativeUserTrigger : condition type must be string (format)")
			end
		end
	end
	
	function newModule.userTriggerExists(self, _name)
		if type(_name) == "string" then
			local localMembers = C_Module_MT:get(self:getIndex())
			local userTriggerEvents = localMembers.userTriggerEvents
					
			return userTriggerEvents[_name] ~= nil
		else
			errorOutputCore(nil, "Function ATME.C_Module.userTriggerExists : name type must be string")
		end
	end
	
	function newModule.getUserTriggerByName(self, _name)
		if type(_name) == "string" then
			local localMembers = C_Module_MT:get(self:getIndex())
			local userTriggerEvents = localMembers.userTriggerEvents
			
			return userTriggerEvents[_name]
		else
			errorOutputCore(nil, "Function ATME.C_Module.getUserTriggerByName : name type must be string")
		end
	end
	
	function newModule.removeUserTrigger(self, _name)
		if type(_name) == "string" then
			local localMembers = C_Module_MT:get(self:getIndex())
			local userTriggerEvents = localMembers.userTriggerEvents
			
			userTriggerEvents[_name] = nil
		else
			errorOutputCore(nil, "Function ATME.C_Module.removeUserTrigger : name type must be string")
		end
	end
	
	return newModule			
end


C_Module_MT.__call = function(t, _name, _handlers, _debugSwitch)
	if type(_name) == "string" and type(_handlers) == "table" and type(_debugSwitch) == "boolean" then
		if isRunning == false then
			local found = false
			for _, _localModuleMembers in C_Module_MT:pairs() do
				if _localModuleMembers.name == _name then
					found = true
				end
			end
			if found == false then
				if (type(_handlers.onCreatePlayerHandler) == "function" or _handlers.onCreatePlayerHandler == nil) and
				   (type(_handlers.onDeletePlayerHandler) == "function" or _handlers.onDeletePlayerHandler == nil) and
				   (type(_handlers.onUpdatePlayerHandler) == "function" or _handlers.onUpdatePlayerHandler == nil) and
				   (type(_handlers.onTakeoffPlayerHandler) == "function" or _handlers.onTakeoffPlayerHandler == nil) and
				   (type(_handlers.onLandingPlayerHandler) == "function" or _handlers.onLandingPlayerHandler == nil) and
				   (type(_handlers.onStartEnginePlayerHandler) == "function" or _handlers.onStartEnginePlayerHandler == nil) and
				   (type(_handlers.onStopEnginePlayerHandler) == "function" or _handlers.onStopEnginePlayerHandler == nil) and
				   (type(_handlers.onCreateAIUnitHandler) == "function" or _handlers.onCreateAIUnitHandler == nil) and
				   (type(_handlers.onDeleteAIUnitHandler) == "function" or _handlers.onDeleteAIUnitHandler == nil) and
				   (type(_handlers.onDisableAIUnitHandler) == "function" or _handlers.onDisableAIUnitHandler == nil) and				   
				   (type(_handlers.onTakeoffAIUnitHandler) == "function" or _handlers.onTakeoffAIUnitHandler == nil) and
				   (type(_handlers.onLandingAIUnitHandler) == "function" or _handlers.onLandingAIUnitHandler == nil) and
				   (type(_handlers.onStartEngineAIUnitHandler) == "function" or _handlers.onStartEngineAIUnitHandler == nil) and
				   (type(_handlers.onStopEngineAIUnitHandler) == "function" or _handlers.onStopEngineAIUnitHandler == nil) and
				   (type(_handlers.onCreateGroupHandler) == "function" or _handlers.onCreateGroupHandler == nil) and
				   (type(_handlers.onDeleteGroupHandler) == "function" or _handlers.onDeleteGroupHandler == nil) and
				   (type(_handlers.onDisableGroupHandler) == "function" or _handlers.onDisableGroupHandler == nil) and
				   (type(_handlers.onCreateStaticObjectHandler) == "function" or _handlers.onCreateStaticObjectHandler == nil) and
				   (type(_handlers.onDeleteStaticObjectHandler) == "function" or _handlers.onDeleteStaticObjectHandler == nil) and
				   (type(_handlers.onTimerHandler) == "function" or _handlers.onTimerHandler == nil) and
				   (type(_handlers.onModuleStartHandler) == "function" or _handlers.onModuleStartHandler == nil) then
				   
					return C_Module_create(_name, _handlers, _debugSwitch)
				else
					errorOutputCore(nil, "Function ATME.C_Module : Module " .. _name .. " - handlers not functions or nil")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Module : Module " .. _name .. " already loaded")
			end
		else
			errorOutputCore(nil, "Function ATME.C_Module : Module " .. _name .. " is not created - ATME is running")
		end
	else
		if type(_name) ~= "string" then
			errorOutputCore(nil, "Function ATME.C_Module : name type must be string")
		elseif type(_handlers) ~= "table" then
			errorOutputCore(nil, "Function ATME.C_Module : handlers type must be table of functions")
		elseif type(_debugSwitch) ~= "boolean" then
			errorOutputCore(nil, "Function ATME.C_Module : debugSwitch type must be boolean")
		end
	end
end

setmetatable(ATME.C_Module,C_Module_MT)

C_Module_MT.mt.__metatable = "Private"
C_Module_MT.mt.__index = ATME.C_Module
C_Module_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Module_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_Module : Bad key " .. k)
	end
end


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Smoke : gère les fumées. Contructeur accessible aux modules externes.
-- Une fumée peut être déclenché simplement. DCS l'arrêtera au bout de 300s (5 minutes). Elle peut être également
-- redéclenchée au bout de cette durée jusqu'à un arrêt volontaire. 
-- La couleur est définie par une string dans ATME.
-- On notera qu'il est aussi possible de déclencher une fumée à partir d'un unité joueur, AI, ou d'un objet statique.
----------------------------------------------------------------------------------------------------------

local C_Smoke_MT = {
	className = "C_Smoke",
	mt = {},
	
	keys = {
		"activate", "stop", "getPoint", "getColor", "getIndex",
	},
	
	localMembers = ATME.C_IndexList(),
	
	add = function(self, _timeStart)
		local item = {
			timeStart = _timeStart,
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

ATME.C_Smoke = {
	getClassName = function(self)
		return C_Smoke_MT.className
	end,
}

local function C_Smoke_create(_color, _point)
	local newSmoke = {}
	local color

	setmetatable(newSmoke, C_Smoke_MT.mt)

	if smokeColors[string.upper(_color)] == -1 then
		color = smokeColors[smokeColorsIndex[math.random(1,#smokeColorsIndex)]]
	else
		color = smokeColors[string.upper(_color)]
	end
	
	local point = ATME.convertToPoint3D(_point)

	local entry = 0
	local activated = false
	
	local index = C_Smoke_MT:add(ATME.getTime())
	
	function newSmoke.activate(self, _always)
		if type(_always) == "boolean" then
			if activated == false then
				trigger.action.smoke(point, color)
				activated = true
				
				if _always == true then
					entry = activatedSmokes:add(self)
				end
			end
		else
			errorOutputCore(nil, "Function ATME.C_Smoke.activate : always type must be boolean")
		end
	end

	function newSmoke.stop(self)
		if entry > 0 then
			activatedSmokes:remove(entry)
			entry = 0
		end
	end
	
	function newSmoke.getPoint(self)
		return point
	end
	
	function newSmoke.getColor(self)
		if color == trigger.smokeColor.Green then
			return smokeColorsIndex[1]
		elseif color == trigger.smokeColor.Red then
			return smokeColorsIndex[2]
		elseif color == trigger.smokeColor.White then
			return smokeColorsIndex[3]
		elseif color == trigger.smokeColor.Orange then
			return smokeColorsIndex[4]
		else
			return smokeColorsIndex[5]
		end
	end
	
	function newSmoke.getIndex(self)
		return index
	end
	
	return newSmoke
end

setmetatable(ATME.C_Smoke,C_Smoke_MT)

C_Smoke_MT.__call = function(t, _color, _point)
	local isPoint = ATME.isPoint(_point)
	if type(_color) == "string" and isPoint ~= POINT_BAD then
		if smokeColors[string.upper(_color)]~= nil then
			return C_Smoke_create(_color, _point)
		else
			errorOutputCore(nil, "Function ATME.C_Smoke : color must be a smoke color")
		end
	else
		if type(_color) ~= "string" then
			errorOutputCore(nil, "Function ATME.C_Smoke : color type must be string")
		elseif isPoint == POINT_BAD then
			errorOutputCore(nil, "Function ATME.C_Smoke : point type must be table (x,y) or (x,y,z)")
		end
	end
end

C_Smoke_MT.mt.__metatable = "Private"
C_Smoke_MT.mt.__index = ATME.C_Smoke
C_Smoke_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Smoke_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_Smoke : Bad key " .. k)
	end
end


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Flare : gère les flares de couleur. Contructeur accessible aux modules externes.
-- Un tir de flare peut être déclenché unitairement. Il peut être aussi redéclenché automatiquement et aléatoirement
-- sur une durée souhaitée ou jusqu'à un arrêt volontaire.
-- La couleur est définie par une string dans ATME. Il existe une couleur "RANDOM" pour un choix aléatoire.
-- On notera qu'il est aussi possible de déclencher un flare à partir d'un unité joueur, AI, ou d'un objet statique.
----------------------------------------------------------------------------------------------------------

local C_Flare_MT = {
	className = "C_Flare",
	mt = {},
	
	keys = {
		"fireOnce", "fire", "stop", "getColor", "getPoint", "getDuration", "getTimeStart",
	},
}

ATME.C_Flare = {
	getClassName = function(self)
		return C_Flare_MT.className
	end,
}

local function C_Flare_create(_color, _point)	
	local newFlare = {}
	local color
	local randomFireColor = false
	local timeStart = 0
	local entry = 0
	local seconds = 0
	
	setmetatable(newFlare, C_Flare_MT.mt)
	
	if flareColors[string.upper(_color)] == -1 then
		randomFireColor = true
	else
		color = flareColors[string.upper(_color)]
	end

	
	local point = ATME.convertToPoint3D(_point)
	
	function newFlare.fireOnce(self)
		if randomFireColor == true then
			color = flareColors[flareColorsIndex[math.random(1,#flareColorsIndex)]]
		end
		
		self:stop()
		
		seconds = 0
		timeStart = ATME.getTime()
		
		trigger.action.signalFlare(point, color, math.random() * 2 * math.pi)
	end
	
	function newFlare.fire(self, _seconds)
		entry = activatedFlares:add(self)
		
		seconds = _seconds
		timeStart = ATME.getTime()
	end
	
	function newFlare.stop(self)
		if entry > 0 then
			activatedFlares:remove(entry)
			entry = 0
		end
	end
	
	function newFlare.getPoint(self)
		return point
	end
	
	function newFlare.getColor(self)
		if color == trigger.flareColor.Green then
			return flareColorsIndex[1]
		elseif color == trigger.flareColor.Red then
			return flareColorsIndex[2]
		elseif color == trigger.flareColor.White then
			return flareColorsIndex[3]
		else
			return flareColorsIndex[4]
		end
	end
	
	function newFlare.getDuration(self)
		return seconds
	end
	
	function newFlare.getTimeStart(self)
		return timeStart
	end
	
	return newFlare
end

setmetatable(ATME.C_Flare,C_Flare_MT)

C_Flare_MT.__call = function(t, _color, _point)
	local isPoint = ATME.isPoint(_point)
	if type(_color) == "string" and isPoint ~= POINT_BAD then
		if flareColors[string.upper(_color)]~= nil then
			return C_Flare_create(_color, _point)
		else
			errorOutputCore(nil, "Function ATME.C_Flare : color must be a flare color or RANDOM")
		end
	else
		if type(_color) ~= "string" then
			errorOutputCore(nil, "Function ATME.C_Flare : color type must be string")
		elseif isPoint == POINT_BAD then
			errorOutputCore(nil, "Function ATME.C_Flare : point type must be table (x,y) or (x,y,z)")
		end
	end
end


C_Flare_MT.mt.__metatable = "Private"
C_Flare_MT.mt.__index = ATME.C_Flare
C_Flare_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Flare_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_Flare : Bad key " .. k)
	end
end
	
----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_GroupSpawnDatas : gère la création dynamique de groupe d'unités AI terrestres uniquement
--
-- Cette classe permet de gérer les chargements/déchargements d'unités d'un véhicule (voir load/unload)
-- des classes C_Player et C_AIUnit. Cette fonctionnalité n'est pas accessible aux autres modules
--
-- Cette classe offre également le spawn d'unités définie à partir d'un groupe existant dans la mission (même nom
-- de groupe et mêmes noms d'unités associées). Le spawn sera en échec si l'unité existe. Elle permet aussi
-- la duplication d'unité à partir d'un group existant dans la mission. Dans ce cas, les unités auront un nom
-- attribué automatiquement "Nom du groupe-UXXX" avec XXX un nombre de 001 à 999. En cas de double, le spawn
-- échouera.
--
-- Pour faire un spawn, il est nécessaire au préalable, que le groupe existe ou pas (en vie), de faire un
-- un duplicateFromMissionDatas. Ceci créera les datas de l'objet qui permettra le spawn.
-- Concernant les WP :
--				- Par défaut : les WP du groupe spawné sont repris
--				- Il est possible d'assigner les WP d'un autre groupe (de même type : vehicle, plane, ship, helicopter)
--				- Il est possible de ne prendre les WP qu'à partir d'un critère de référence (position, nom de WP de la
--				  route reprise, offset de WP de la route repris (1 à n, 0 correspondant au WP 0sur l'éditeur de mission),
--				  ou enfin, au plus proche d'un objet spécifié (Unité AI, joueur ou static)
--
-- ATTENTION : WP 0 dans l'éditeur de mission est dans waypoints[1] avec les coordonnées de la première
-- unité du groupe.
----------------------------------------------------------------------------------------------------------

-- Prédéfinitions car utilisé avant déclaration

local C_Group_delete
local C_AIUnit_delete
local C_Player_delete
local C_Player_addOnBoardUnit
local C_Player_MT
local C_AIUnit_MT
local C_Group_MT
local C_GroupSpawnDatas_MT 

local function C_GroupSpawnDatas_copyWaypoint(_waypointOrig, _waypointDest)
	for _key, _value in pairs(_waypointOrig) do
		_waypointDest[_key] = _value
	end
end

local function C_GroupSpawnDatas_setWaypoints(_ATMEGroup, _waypoints)

	-- On ajoute la position courante à la nouvelle route

	local newWP = {}
	C_GroupSpawnDatas_copyWaypoint(_waypoints[1], newWP)
	newWP.action = "Off Road"
	newWP.x = _ATMEGroup:getFirstUnit():getPosition().x
	newWP.y = _ATMEGroup:getFirstUnit():getPosition().z

	table.insert(_waypoints, 1, newWP)

	-- On active la nouvelle route
	
	local controller = _ATMEGroup:getDCSGroup():getController()
	local _task = { 
		id = 'Mission', 
		params = { 
			route = {
				points = _waypoints,
			}
		} 
	}

	controller:setTask(_task)
end

local function C_GroupSpawnDatas_getWaypoints(_OrigGroupName)
	local initDatas = allGroupsInMission[_OrigGroupName]
	local waypoints = {}
	for _, _waypoint in ipairs(initDatas.waypoints) do
		table.insert(waypoints, _waypoint)
	end
		
	return waypoints
end

local function C_GroupSpawnDatas_startRouteAt(_waypoints, _reference)
	if type(_reference) == "string" then
		local found = false
		local name = string.upper(_reference)
		
		while  #_waypoints > 1 and found == false do
			local wpName = string.upper(_waypoints[2].name)
			if name ~= wpName and found == false then
				table.remove(_waypoints, 2)
			elseif name == wpName then
				found = true
			end
		end
	elseif type(_reference) == "number" then
		if _reference > 0 and _reference + 1<= #_waypoints then
			local nb = _reference - 1
			while nb > 0 do 
				table.remove(_waypoints, 2)
				nb = nb - 1
			end
		end
	else
		local point
		if ATME.isPoint(_reference) ~= POINT_BAD then
			point = ATME.convertToPoint3D(_reference)
		else
			point = _reference:getPosition()
		end
		
		local index = 1
		local minDistance = -1
		
		for _index, _waypoint in ipairs(_waypoints) do
			if _index > 0 then
				local v = ATME.C_Vector3D(point, _waypoint)
				if minDistance < 0 or minDistance > v:getHModule() then
					minDistance = v:getHModule()
					index = _index
				end
			end
		end
		
		for _i = 2, index - 1 do
			table.remove(_waypoints, 2)
		end	
	end

	--[[ DEBUG
	local text
	if #_waypoints then
		text = "Liste WP ...\n"
		for _i, wp in ipairs(_waypoints) do
			text = text .. "  -> WP-" .. _i .. " - x = " .. wp.x .. " / y = " .. wp.y .. "/ alt = " .. wp.alt .. "\n"
		end
	else
		text = "Pas de WP"
	end
	outputCore(nil, text, 1)
	]]--
end



-- Unités ayant déclenché un S_EVENT_BIRTH pendant un addGroup. Parfois le groupe DCS n'est
-- alors pas encore initialisé. Aussi, elles sont ensuite intégrées à ATME dès la fin d'exécution
-- de addgroup.
local unitsReadyToCreate = {}  	
								
local C_AIUnit_create -- Prédéclaration

-- Ne concerne que les véhicules terrestres pour le débarquement des troupes embarquées
local function C_SpawnGroup_spawnFromObject(_spawnGroupDatas, _object)
	local localMembers = C_GroupSpawnDatas_MT:get(_spawnGroupDatas:getIndex())

	local nb = _object:getNbUnitsOnBoard()
	
	-- Min une unité par 3° soit 118 unités au maximum
	-- Calcul : pour 180° -> 180/3 - 1 unités, soit pour 360°, 2 x (180/3 - 1) = 118
	-- Max units par groupes terrestre = 99 < 118 donc pas de soucis actuellement

	local point = _object:getPosition()

	local azimuth = _object:getAzimuth()
	
	-- Vérification d'absence d'eau côté droit/gauche
	
	local sides = {}
	local startAngle = 0
	local nbPointsWater = {0,0}
	local index = 1
	for _side = 1,-1,-2 do
		for _angle = 45 * _side, 135 * _side, 45 * _side do
			local result = ATME.rotationH({x = point.x + 10 * math.cos(_angle), y = point.z + 10 * math.sin(_angle)}, point, -azimuth)
			if ATME.isSurfaceWater(result) == true then
				nbPointsWater[index] = nbPointsWater[index] + 1
			end
		end
		
		if nbPointsWater[index] < 2 then
			table.insert(sides, startAngle)
		end
		startAngle = startAngle + 180
		index = index + 1
	end

	if #sides > 0 then
		if #sides == 2 then
			if nbPointsWater[1] > nbPointsWater[2] then -- On permute les côtés pour privilégier le côté sans eau (1 unité de plus si impair)
				sides[1] = 180
				sides[2] = 0
			end
		end
		-- Ajustements en fonction du nombre d'unités et du nombre
		-- de côtés possibles.
		
		-- Si moins de 2 unités par côté pas de symétrie
		if nb/#sides <= 2 and #sides > 1 then
			table.remove(sides,#sides)
		end
		
		-- Si symétrie (2 côtés) et nombre impair d'unités -> On ajuste pour rendre pairs
		if nb/#sides ~= math.floor(nb/#sides) then
			-- Si le nombre d'unités est impair quand 2 côtés ok, on ajoute 1
			nb = nb + 1
		end

		-- Calcul de l'angle entre chaque unité
		
		local deltaAngle = 180 / (nb + 1)
		
		local distances = {}

		table.insert(distances, personnelCarrierAttributes[_object:getTypeName()].minDistance)
		
		if deltaAngle < 3 then
			table.insert(distances, personnelCarrierAttributes[_object:getTypeName()].minDistance + 2)
		end 

		local unitAngle = startAngle
		local id = 1
		local currentSide = 1
		local currentDistance = 1
		
		local nbDeadUnits = 0

		while id <= #localMembers.datas.units do
			local unit = localMembers.datas.units[id]
			unitAngle = unitAngle + deltaAngle


			local unitAngleRad = math.rad(unitAngle + sides[currentSide])
			currentSide = currentSide + 1
			if currentSide > #sides then
				currentSide = 1
			end
		
			local result = ATME.rotationH(
				{
					x = point.x + distances[currentDistance] * math.cos(unitAngleRad), 
					y = point.z + distances[currentDistance]  * math.sin(unitAngleRad),
				}, 
				point, 
				-azimuth)
			
			currentDistance = currentDistance + 1
			if currentDistance > #distances then
				currentDistance = 1
			end
			
			-- Si l'unité est bien sur terre, alors ok
			if ATME.isSurfaceWater(result) == false then
			
				unit.x = result.x
				unit.y = result.z

				unit.heading = unitAngleRad + math.rad(azimuth)
				id = id + 1
			else -- si elle est dans l'eau alors elle meure (elle ne sera pas réactivée).
				table.remove(localMembers.datas.units, id)
				nbDeadUnits = nbDeadUnits + 1
			end
		end
		
		if #localMembers.datas.units > 0 then
			local waypoints = C_GroupSpawnDatas_getWaypoints(localMembers.waypointsGroupName)

			C_GroupSpawnDatas_startRouteAt(waypoints, _object)
			localMembers.datas.route = {}
			localMembers.datas.route.points = waypoints
			
			-- Mise à jour de la position du premier WP (WP0 dans l'éditeur) avec
			-- les nouvelles coordonnées de la première unité du groupe qui débarque.
			-- Si le groupe est en maintenir position (task = Hold), cette tache est supprimée
			
			localMembers.datas.route.points[1].x = localMembers.datas.units[1].x
			localMembers.datas.route.points[1].y = localMembers.datas.units[1].y
			localMembers.datas.route.points[1].action = "Off Road"
			
			local index = 0
			for _i, _task in ipairs(localMembers.datas.route.points[1].task.params.tasks) do
				if _task.id == "Hold" then -- Maintenir position activé
					index = _i
				end
			end
			
			if index ~= 0 then
				table.remove(localMembers.datas.route.points[1].task.params.tasks, index)
			end

			local rollAxis = ATME.C_Line2D(_object:getRollAxisVector(), _object:getPosition())
			
			local pAxis
			local newWP = {}
			C_GroupSpawnDatas_copyWaypoint(localMembers.datas.route.points[1], newWP)

			if #localMembers.datas.route.points > 1 then
				-- Les unités ont tendance à détruire le transport si elles le percutent (bug DCS)
				-- Aussi, un WP est ajouté dans l'axe du transport à 100m, au plus près du
				-- deuxième WP (WP1 dans l'éditeur de mission ou créé s'il n'existe pas) si
				-- le groupe dispose d'une route de plus d'un WP.
				
				pAxis = {}
				pAxis[1] = rollAxis:getPointFromOrigin(100)
				pAxis[2] = rollAxis:getPointFromOrigin(-100)
				
				local indexPAxis = 1
				local vAxis1 = ATME.C_Vector3D(pAxis[1], localMembers.datas.route.points[2])
				local vAxis2 = ATME.C_Vector3D(pAxis[2], localMembers.datas.route.points[2])
				
				if vAxis2:getHModule() < vAxis1:getHModule() then
					indexPAxis = 2
				end
				
				newWP.x = pAxis[indexPAxis].x
				newWP.y = pAxis[indexPAxis].z
				table.insert(localMembers.datas.route.points, 2, newWP)
			else
				-- Si le groupe ne dispose pas de route de plus d'un WP, on en crée un à 500 devant
				-- l'unité de transport. Ainsi le déplacement du groupe est maitrisé par l'azimuth du 
				-- transport.
				
				pAxis = rollAxis:getPointFromOrigin(300)
				newWP.x = pAxis.x
				newWP.y = pAxis.z
				table.insert(localMembers.datas.route.points, newWP)
			end
			
			coalition.addGroup(localMembers.country, localMembers.category, localMembers.datas)
			
			-- Les unités sont créées (event S_EVENT_BIRTH) pendant le addgroup mais parfois
			-- leur groupe associé ne l'est pas encore ... Donc on mémorise et on traite ci après
			
			while #unitsReadyToCreate > 0 do
				C_AIUnit_create(unitsReadyToCreate[#unitsReadyToCreate])
				table.remove(unitsReadyToCreate, #unitsReadyToCreate)
			end
			
			
			
			local localGroupMembers = C_Group_MT:get(groups[localMembers.datas.name]:getIndex())
			localGroupMembers.origineMissionDatasGroupName = localMembers.initDatas.name
			localGroupMembers.waypointsGroupName = localMembers.waypointsGroupName
			
			return false, groups[localMembers.datas.name], nbDeadUnits
		else
			return true, "PLAYER_UNLOAD_ALL_GROUP_DEAD", 0
		end
	else
		return true, "PLAYER_UNLOAD_ONLY_WATER", 0
	end
end

-- Fonction reprenant les infos de mission pour le spawn de groupe :
-- 		- si _toSpawnUnitNames est nil alors celà vient de duplicateFromMissionDatas donc la duplication d'un groupe existant ou pas
-- 		- si _newName est nil alors c'est un unload de player ou d'unité AI -> débarquement de troupes
-- En l'état, _toSpawnUnitNames et _newName ne peuvent être être différents de nil ou à nil tous les deux ensembles -> Anormal
local function C_GroupSpawnDatas_createFromMissionInfos(_groupName, _origineMissionDatasGroupName, _waypointsGroupName, _toSpawnUnitNames, _newName)
	local currentGroupName = _groupName
	local origineMissionDatasGroupName = _origineMissionDatasGroupName
	local waypointsGroupName = _waypointsGroupName
	
	-- DEBUG outputCore(nil, "Actual name = " ..currentGroupName .. " - origine = " .. origineMissionDatasGroupName .. " - WPName = " ..waypointsGroupName, 1)

	local initDatas = allGroupsInMission[origineMissionDatasGroupName]
		
	if initDatas.type ~= "plane" and initDatas.type ~= "helicopter" then -- pour l'instant on limite les possibilités
		local newSpawnGroup = {}
		
		
		setmetatable(newSpawnGroup, C_GroupSpawnDatas_MT.mt)
		
		local index = C_GroupSpawnDatas_MT:add(initDatas, currentGroupName, waypointsGroupName)

		local localMembers = C_GroupSpawnDatas_MT:get(index)
		
		if _newName ~= nil then
			localMembers.datas.name = _newName
			maxGroupId = maxGroupId + 1
			localMembers.datas.groupId = maxGroupId
		end
		
		if _toSpawnUnitNames ~= nil then
			for _i, _unitName in ipairs(_toSpawnUnitNames) do
				local unitName
				if origineMissionDatasGroupName ~= currentGroupName then
					local i,j = string.find(_unitName, "-")
					
					unitName = string.sub(_unitName, i+1)
					-- DEBUG outputCore(nil," nom actuel = " .. _unitName .. " / nom origine = " .. unitName, 1)
				else
					unitName = _unitName
				end
				
				currentUnit = initDatas.units[unitName]
				
				localMembers.datas.units[#localMembers.datas.units + 1] = {
					type = currentUnit.type,
					name = _unitName,
					skill = currentUnit.skill,
					x = currentUnit.x,
					y = currentUnit.y,
					alt = currentUnit.alt,
					alt_type = currentUnit.alt_type,
					playerCanDrive = currentUnit.playerCanDrive,
					heading = currentUnit.heading,
				}
			end
		else
			local i = 1
			for _unitName, _currentUnit in pairs(initDatas.units) do
				local unitName = _unitName
				if _newName ~= nil then
					unitName = string.format("SpawnU#%05d-%s", totalAIUnits + i, _unitName)
				end
				
				i = i + 1
				
				localMembers.datas.units[#localMembers.datas.units + 1] = {
					type = _currentUnit.type,
					name = unitName,
					skill = _currentUnit.skill,
					x = _currentUnit.x,
					y = _currentUnit.y,
					alt = _currentUnit.alt,
					alt_type = _currentUnit.alt_type,
					playerCanDrive = _currentUnit.playerCanDrive,
					heading = _currentUnit.heading,
				}
			end
		end

		if initDatas.waypoints ~= nil and #initDatas.waypoints > 0 and _newName == nil then
			for _, _waypoint in ipairs(initDatas.waypoints) do
				table.insert(localMembers.waypoints, _waypoint)
			end
		end
		
		function newSpawnGroup.getIndex(self)
			return index
		end
		
		function newSpawnGroup.getName(self)
			return localMembers.datas.name
		end
		
		function newSpawnGroup.spawn(self, ...)
			local args = {...}
			local waypointsGroupName
			local reference
			local point = {}
			
			if #args > 0 then
				if #args >= 1 and (ATME.isPoint(args[1]) ~= POINT_BAD or type(args[1]) == "string") then
					if ATME.isPoint(args[1]) ~= POINT_BAD then
						point.x = args[1].x
						point.y = args[1].y
						point.z = args[1].z
					else
						local zone = ATME.getDCSZone(args[1])
						
						if zone ~= nil then
							local angle = 2 * math.pi * math.random()
							local radius = math.random(0, zone.radius)
							point.x = zone.point.x + radius * math.sin(angle)
							point.y = zone.point.z + radius * math.cos(angle)
						else
							errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.spawn : DCS zone " .. args[1] .. " unknown")
							return
						end
					end
					if #args >= 2 and type(args[2]) == "string" then
						waypointsGroupName = args[2]
						
						if allGroupsInMission[waypointsGroupName] ~= nil then
							if #args == 3 and (ATME.isObjectClass(args[3], ATME.C_AIUnit) == true or ATME.isObjectClass(args[3], ATME.C_Player) or 
									ATME.isObjectClass(args[3], ATME.C_StaticObject) or ATME.isPoint(args[3]) ~= POINT_BAD or
									type(args[3]) == "string" or type(args[3]) == "number") then
							
								reference = args[3]
							else
								if #args >= 3 then
									errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.spawn : arg#3 type must be string or number or ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
									return
								end
								
								reference = 1
							end
						else
							errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.spawn : " .. waypointsGroupName.. " doesn't exist in mission datas")
							return
						end
					else
						if #args >= 2 then
							errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.spawn : arg#2 must be string")
							return
						elseif #args == 1 then
							waypointsGroupName = localMembers.waypointsGroupName
							reference = 1
						end
					end
				else
					errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.spawn : arg#1 type must be string or table (x,y) or (x,y,z) ")
					return
				end
			else
				point.x = localMembers.datas.units[1].x
				point.y = localMembers.datas.units[1].y
				waypointsGroupName = localMembers.waypointsGroupName
				reference = 1
			end
				
			if ATME.C_Group.exists(localMembers.datas.name) == false and groupsOnBoard[localMembers.datas.name] == nil then
				if initDatas.type == allGroupsInMission[waypointsGroupName].type then	
					local point3D = ATME.convertToPoint3D(point)
					local index = 0
					
					for _i, _unit in ipairs(localMembers.datas.units) do
						if ATME.C_AIUnit.exists(_unit.name) == true then
							index = _i
						end
					end
					
					if index == 0 then
						local first = true
						local position0 = {}
						for _i, _unit in ipairs(localMembers.datas.units) do
							if first == true then
								position0.x = _unit.x
								position0.y = _unit.y
								first = false
							end
							
							_unit.x = point3D.x + _unit.x - position0.x
							_unit.y = point3D.z + _unit.y - position0.y
						end
						
						local waypoints = C_GroupSpawnDatas_getWaypoints(waypointsGroupName)
						C_GroupSpawnDatas_startRouteAt(waypoints, reference)
						localMembers.datas.route = {}
						localMembers.datas.route.points = waypoints
						localMembers.datas.route.points[1].x = localMembers.datas.units[1].x
						localMembers.datas.route.points[1].y = localMembers.datas.units[1].y
						
						local alt  = ATME.getSurfaceAltitude({x = localMembers.datas.route.points[1].x, y = localMembers.datas.route.points[1].y}) + math.random(300,600)
						if alt < localMembers.datas.route.points[1].alt then
							alt = localMembers.datas.route.points[1].alt
						end
						localMembers.datas.route.points[1].alt = alt
						
						coalition.addGroup(localMembers.country, localMembers.category, localMembers.datas)
						
						-- Les unités sont créées (event S_EVENT_BIRTH) pendant le addgroup mais parfois
						-- leur groupe associé ne l'est pas encore ... Donc on mémorise et on traite ci après
			
						while #unitsReadyToCreate > 0 do
							C_AIUnit_create(unitsReadyToCreate[#unitsReadyToCreate])
							table.remove(unitsReadyToCreate, #unitsReadyToCreate)
						end

						-- L'affectation des instances C_Group et C_AIUnit est faite par l'événement BIRTH déclenchée par le addGroup
						-- Ces événements ont eu lieu avant la fin de la fonction addGroup, donc C_Group est correctement initialisé
						
						-- On mémorise le nom d'origine du groupe, celui définit dans la mission...
						-- Ce sera nécessaire si c'est un groupe d'infanterie qui sera ultérieurement chargé dans une unité de transport
						-- et le groupe d'origine des waypoints attribués.
						
						local localGroupMembers = C_Group_MT:get(groups[localMembers.datas.name]:getIndex())
						localGroupMembers.origineMissionDatasGroupName = initDatas.name
						localGroupMembers.waypointsGroupName = waypointsGroupName
						
						return false, groups[localMembers.datas.name]
					else
						errorOutputCore(nil,"Function ATME.C_GroupSpawnDatas.spawn : Unit " .. localMembers.datas.units[index].name .. " already exists")
						return true, "GLOBAL_ERROR"
					end
				else
					errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.spawn : " .. waypointsGroupName .. " must have same type as " .. localMembers.datas.name)
					return
				end
			else
				errorOutputCore(nil,"Function ATME.C_GroupSpawnDatas.spawn : Group " .. localMembers.datas.name .. " already exists or on board")
				return true, "GLOBAL_ERROR"
			end
		end
		
		function newSpawnGroup.getWaypointByIndex(self, _id)
			if type(_id) == "number" then
				if _id > 0 and _id <= #localMembers.waypoints then
					local id = math.floor(_id)
					return localMembers.waypoints[id].x, localMembers.waypoints[id].alt, localMembers.waypoints[id].y
				else
					errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.getWaypointByIndex : id is bad index waypoint")
				end
			else
				errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.getWaypointByIndex : id type must be number")
			end
		end
			
		return newSpawnGroup
	end

end

-- Prédéclarée avant en local
C_GroupSpawnDatas_MT = {
	className = "C_GroupSpawnDatas",
	mt = {},
	
	keys = {
		"getIndex", "getName", "getWaypointByIndex", "spawn",
	},
	
	localMembers = ATME.C_IndexList(),
	
	add = function(self, _initDatas, _currentGroupName, _waypointsGroupName)
		local item = {
			initDatas = _initDatas,
			currentGroupName = _currentGroupName,
			waypointsGroupName = _waypointsGroupName,
			country = _initDatas.country,
			category = _initDatas.category,
			waypoints = {},
			datas = {
				units = {},
				name = _currentGroupName,
				visible = false,
				taskSelected = true,
				lateActivation = false,
				tasks = {},
				hidden = _initDatas.hidden,
				start_time = 0,
				task = "Ground Nothing",
			}
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:get(_id)._itemCommandsList = nil
		self.localMembers:get(_id)._F10SubMenusList = nil
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

ATME.C_GroupSpawnDatas = {
	
	getClassName = function(self)
		return C_GroupSpawnDatas_MT.className
	end,
	
	duplicateFromMissionDatas = function(_groupNameInMission, _newGroupName)
		if type(_groupNameInMission) == "string" and type(_newGroupName) == "string" then
			local initDatas = allGroupsInMission[_groupNameInMission]
			local initDatasCopy = allGroupsInMission[_newGroupName]
			if initDatas ~= nil then
				if initDatasCopy == nil then
					return false, C_GroupSpawnDatas_createFromMissionInfos(_groupNameInMission, _groupNameInMission, _groupNameInMission, nil, _newGroupName)
				else
					errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.duplicateFromMissionDatas : datas found in mission data for new group " .. _newGroupName .. " - can't duplicate")
					return true, "GLOBAL_ERROR"
				end
			else
				errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.duplicateFromMissionDatas : Group name " .. _groupNameInMission .. " doesn't exist in mission datas")
				return true, "GLOBAL_ERROR"
			end
		else
			if type(_groupNameInMission) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.duplicateFromMissionDatas : groupNameInMission type must be string")
			elseif type(_newGroupName) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas.duplicateFromMissionDatas : newGroupName type must be string")
			end
			
			return true, "GLOBAL_ERROR"
		end
	end,
}

C_GroupSpawnDatas_MT.mt.__metatable = "Private"
C_GroupSpawnDatas_MT.mt.__index = ATME.C_GroupSpawnDatas
C_GroupSpawnDatas_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_GroupSpawnDatas_MT.keys) do
		if k == key then
			found = true
		end
	end

	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_GroupSpawnDatas : Bad key " .. k)
	end
end


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Group : gère les groupes d'unités (AI ou joueur) au format ATME. Pas de contructeur accessible
-- pour les modules externes.
-- Les instances de cette classe sont intégralement gérées par le module ATME_Core. La création et la destruction
-- dépendent du contexte de début de mission et des événements qui se produisent dans DCS en cours de mission.
-- Les modules externes accèdent à cette gestion indirectement par les handlers définis dans ceux-ci et les méthodes
-- accessibles de cette classe. Ceci permet de garantir une cohérence sur les listes internes ATME.
-- ATME crée des instances de cette classe à partir de la création d'instance d'unité joueur ou AI.
-- Chaque unité AI ou joueur dispose d'une variable représentant le groupe ATME qui lui est associé.
-- En cas de destruction (suppression) d'une unité (AI ou joueur), le groupe ne sera détruit qu'après la destruction
-- de la dernière unité (AI ou joueur) du groupe.
----------------------------------------------------------------------------------------------------------

-- C_Group_MT prédéclarée avant en local
C_Group_MT = {
	className = "C_Group",
	mtModules = {},
	mt = {},
	
	keys = {
		"disable", "setSignal", "resetSignal", "isSignalSet", "getFirstUnit", "getNextUnit", "getPreviousUnit", "getLastUnit",
		"getUnitByName", "getUnits", "getNbUnits", "getCoalitionName", "getCountryName", "isOnlyInfantry", "isOnlyAI", "isInDCSZone",
		"isInZone2D", "isInZone3D", "isNear", "getCategoryName", "setPickupZone", "resetPickupZone", "hasPickupZone", "getPickupZoneName", 
		"isReadyToBoard", "changeReadyToBoard", "isBoardingStarted", "isStopped", "move", "stop", "freeze", "getDCSGroup", 
		"getName", "getIndex", "activate", "isActivated", "setRoute", "getMinDistance", "getMaxDistance", "getMinHDistance", "getMaxHDistance", 
		"getBarycentre", "modules",
	},
	
	localMembers = ATME.C_IndexList(),
	
	add = function(self, _hasPlayer, _name)
		local item = {
			origineMissionDatasGroupName = _name,
			waypointsGroupName = _name,
			hasPlayers = _hasPlayer,
			onlyInfantry = true,
			boardingStarted = false,
			signalIndex = 0,
			pickupZoneName = "",
			pickupZoneIndex = 0,
			isEnabled = true,
			isStopped = false,
			nextUnit = 0,
			previousUnit = 0,
			ATMEUnits = {},
			ATMEUnitNames = {},
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:get(_id).ATMEUnits = nil
		
		while #self.localMembers:get(_id).ATMEUnitNames > 0 do
			table.remove(self.localMembers:get(_id).ATMEUnitNames, #self.localMembers:get(_id).ATMEUnitNames)
		end
		self.localMembers:get(_id).ATMEUnitNames = nil
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

ATME.C_Group = {
	getClassName = function(self)
		return C_Group_MT.className
	end,
	
	exists = function(_groupName)
		if type(_groupName) == "string" then
			return groups[_groupName] ~= nil
		else
			errorOutputCore(nil, "Function ATME.C_Group.exists : name type must be string")
		end
	end,
	
	getByName = function(_groupName)
		if type(_groupName) == "string" then
			return groups[_groupName]
		else
			errorOutputCore(nil, "Function ATME.C_Group.getByName : name type must be string")
		end
	end,
	
	getGroupsInDCSZoneForAll = function(_zoneName, _allGroup)
		if type(_zoneName) == "string" and type(_allGroup) == "boolean" then
			local zone = ATME.getDCSZone(_zoneName)
			if zone ~= nil then
				local groupsInZone = {}
				for _, _group in pairs(groups) do
					local distance
					if _allGroup == false then
						distance = _group:getMinHDistance(zone.point)
					else
						distance = _group:getMaxHDistance(zone.point)
					end
					
					if distance <= zone.radius then
						groupsInZone[_group:getName()] = _group
					end
				end
				
				return groupsInZone
			else
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInDCSZoneForAll : Zone " .. _name .. " unknown")
			end
		else
			if type(_zoneName) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInDCSZoneForAll : zoneName type must be string")
			elseif type(_allGroup) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInDCSZoneForAll : allGroup type must be boolean")
			end
		end
	end,
	
	getGroupsInDCSZoneForCoalition = function(_coalition, _zoneName, _allGroup)
		if type(_coalition) == "string" and type(_zoneName) == "string" and type(_allGroup) == "boolean" then
			if coalitionIds[_coalition] ~= nil then
				local zone = ATME.getDCSZone(_zoneName)
				if zone ~= nil then
					local groupsInZone = {}
					for _, _group in pairs(groups) do
						local distance
						if _allGroup == false then
							distance = _group:getMinHDistance(zone.point)
						else
							distance = _group:getMaxHDistance(zone.point)
						end
						
						if _group:getCoalitionName() == _coalition and distance <= zone.radius then
							groupsInZone[_group:getName()] = _group
						end
					end
					
					return groupsInZone
				else
					errorOutputCore(nil, "Function ATME.C_Group.getGroupsInDCSZoneForCoalition : coalition bad name")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInDCSZoneForCoalition : DCS zone " .. _zoneName .. " unknown")
			end
		else
			if type(_coalition) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInDCSZoneForCoalition : coalition type must be string")
			elseif type(_zoneName) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInDCSZoneForCoalition : zoneName type must be string")
			elseif type(_allGroup) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInDCSZoneForCoalition : allGroup type must be boolean")
			end
		end
	end,

	getGroupsInZone2DForAll = function(_reference, _radius, _allGroup)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD) and
			type(_radius) == "number" and type(_allGroup) == "boolean" then
			
			local point
			if ATME.isPoint(_reference) ~= POINT_BAD then
				point = ATME.convertToPoint3D(_reference)
			else
				point = _reference:getPosition()
			end
			
			local groupsInZone = {}
			for _, _group in pairs(groups) do
				local distance
				if _allGroup == false then
					distance = _group:getMinHDistance(point)
				else
					distance = _group:getMaxHDistance(point)
				end
				
				if  distance <= _radius then
					groupsInZone[_group:getName()] = _group
				end
			end
			
			return groupsInZone
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInZone2DForAll : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInZone2DForAll : radius type must be number")
			elseif type(_allGroup) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInZone2DForAll : allGroup type must be boolean")
			end
		end
	end,
	
	getGroupsInZone2DForCoalition = function(_coalition, _reference, _radius, _allGroup)
		if type(_coalition) == "string" and (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" and type(_allGroup) == "boolean" then
				
			if coalitionIds[_coalition] ~= nil then
				local point
				if ATME.isPoint(_reference) ~= POINT_BAD then
					point = ATME.convertToPoint3D(_reference)
				else
					point = _reference:getPosition()
				end
				
				local groupsInZone = {}
				for _, _group in pairs(groups) do
					local distance
					if _allGroup == false then
						distance = _group:getMinHDistance(point)
					else
						distance = _group:getMaxHDistance(point)
					end
					
					if _group:getCoalitionName() == _coalition and distance <= _radius then
						groupsInZone[_group:getName()] = _group
					end
				end
				
				return groupsInZone
			else
				errorOutputCore(nil, "Function ATME.C_Group.getInDCSZoneForCoalition : coalition bad name")
			end
		else
			if type(_coalition) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInZone2DForCoalition : coalition type must be string")
			elseif ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInZone2DForCoalition : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInZone2DForCoalition : radius type must be number")
			elseif type(_allGroup) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Group.getGroupsInZone2DForCoalition : allGroup type must be boolean")
			end
		end
	end,
	
	getReadyToBoardGroupsForAll = function()
		local groupsReady = {}
				
		for _, _group in pairs(readyToBoardGroups) do
			groupsReady[_group:getName()] = _group
		end
		
		return groupsReady
	end,
	
	getReadyToBoardGroupsForCoalition = function(_coalition)
		if type(_coalition) == "string" then
			if coalitionIds[_coalition] ~= nil then
				local groupsReady = {}
				
				for _, _group in pairs(readyToBoardGroups) do
					if _group:getCoalitionName() == _coalition then
						groupsReady[_group:getName()] = _group
					end
				end
				
				return groupsReady
			else
				errorOutputCore(nil, "Function ATME.C_Group.getReadyToBoardGroupsForCoalition : coalition bad name")
			end
		else
			errorOutputCore(nil, "Function ATME.C_Group.getReadyToBoardGroupsForCoalition : coalition type must be string")
		end
	end,
}


local function C_Group_addUnit(_ATMEGroup, _unit)
	local unitName = _unit:getName()

	local localMembers = C_Group_MT:get(_ATMEGroup:getIndex())
	
	if ATME.isObjectClass(_unit, ATME.C_Player) == true then
		localMembers.hasPlayers = true
	end
	
	table.insert(localMembers.ATMEUnitNames, unitName)
	localMembers.ATMEUnits[unitName] = _unit
	
	outputCore(nil,"Function C_Group_addUnit : ajout unité " .. unitName, 2)
	
	if ATME.isObjectClass(_unit, ATME.C_Player) == true or 
		(ATME.isObjectClass(_unit, ATME.C_AIUnit) == true and localMembers.onlyInfantry == true and _unit:isInfantry() == false) then
		localMembers.onlyInfantry = false
	end
end


local function C_Group_deleteUnit(_ATMEGroup, _unitName)
	local localMembers = C_Group_MT:get(_ATMEGroup:getIndex())
	localMembers.ATMEUnits[_unitName] = nil
	
	local key = 0
	for _i, _name in ipairs(localMembers.ATMEUnitNames) do
		-- DEBUG outputCore(nil,"Function C_Group_deleteUnit : trouvée " .. _name, 1)
		if _name == _unitName then
			key = _i
		end
	end
	
	table.remove(localMembers.ATMEUnitNames, key)
	
	if #localMembers.ATMEUnitNames == 0 then
		outputCore(nil,"Function C_Group_deleteUnit : appel C_group_delete", 2)
		C_Group_delete(_ATMEGroup)
	end
end

local function C_Group_evalDistances(_ATMEGroup, _point)
	local minDistance = -1
	local maxDistance = -1
	local minHDistance = -1
	local maxHDistance = -1
	local minDeltaAltitude = -1
	local maxDeltaAltitude = -1
	local units = _ATMEGroup:getUnits()
	
	for _, _unit in pairs(units) do
		local v = ATME.C_Vector3D(_point, _unit:getPosition())

		if minDistance < 0 or minDistance > v:getModule() then
			minDistance = v:getModule()
		end
		
		if maxDistance < v:getModule() then
				maxDistance = v:getModule()
		end
		
		if minHDistance < 0 or minHDistance > v:getHModule() then
			minHDistance = v:getHModule()
		end
		
		if maxHDistance < v:getHModule() then
				maxHDistance = v:getHModule()
		end
		
		if minDeltaAltitude < 0 or minDeltaAltitude > v:getY() then
			minDeltaAltitude = v:getY()
		end
		
		if maxDeltaAltitude < v:getY() then
			maxDeltaAltitude = v:getY()
		end
	end
	
	return minDistance, maxDistance, minHDistance, maxHDistance, minDeltaAltitude, maxDeltaAltitude
end

local function C_Group_create(_ATMEUnit)
	local group = _ATMEUnit:getDCSUnit():getGroup()
	local name = group:getName()
		
	local newGroup = {}
	setmetatable(newGroup, C_Group_MT.mt)
	
	local index
	
	if ATME.isObjectClass(_ATMEUnit, ATME.C_Player) == true then
		index = C_Group_MT:add(true, name)
	else
		index = C_Group_MT:add(false, name)
	end
	
	function newGroup.getName(self)
		return name
	end
	
	function newGroup.getCategoryName(self)
		local id =  group:getCategory()
		return categoryNames[id]
	end
	
	function newGroup.getCoalitionName(self)
		local id = group:getCoalition()
		return coalitionNames[id]
	end
	
	function newGroup.getDCSGroup(self)
		return group
	end
	
	function newGroup.getIndex(self)
		return index
	end
		
	function newGroup.activate(self)
		group:activate()
	end
	
	function newGroup.isActivated(self)
		-- ATTENTION fonction DCS sur l'unité et non sur le groupe
		-- Risque de BUG DCS car pas homogène
		
		local unit = self:getFirstUnit()

		return unit:getDCSUnit():isActive()
	end
	
	function newGroup.setRoute(self, ...)
		local args = {...}
		local groupName
		local reference
		local waypoints
		
		if #args > 0 then
			if #args == 2 and type(args[1]) == "string" then
				groupName = args[1]

				if allGroupsInMission[groupName] ~= nil then
					if ATME.isObjectClass(args[2], ATME.C_AIUnit) == true or ATME.isObjectClass(args[2], ATME.C_Player) or 
							ATME.isObjectClass(args[2], ATME.C_StaticObject) or ATME.isPoint(args[2]) ~= POINT_BAD or
							type(args[2]) == "string" or type(args[2]) == "number" then
						reference = args[2]
					else
						errorOutputCore(nil, "Function ATME.C_Group.setRoute : arg#2 type must be string or number or ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
					end
				else
					errorOutputCore(nil, "Function ATME.C_Group.setRoute : " .. groupName.. " doesn't exist in mission datas")
					return
				end
			else
				if type(args[1]) ~= "string" then
					errorOutputCore(nil, "Function ATME.C_Group.setRoute : arg#1 type must be string")
				elseif #args ~= 2 then
					errorOutputCore(nil, "Function ATME.C_Group.setRoute : must have 0 or 2 parameters")
				end
				return
			end
		else
			groupName = self:getName()
			reference = 1
		end
		
		local localMembers = C_Group_MT:get(index)
		localMembers.waypointsGroupName = groupName	-- Mémorisation du groupe à l'origine des waypoints.
		
		waypoints = C_GroupSpawnDatas_getWaypoints(groupName)
		
		C_GroupSpawnDatas_startRouteAt(waypoints, reference)
		C_GroupSpawnDatas_setWaypoints(self, waypoints)
	end
	
	function newGroup.getMinDistance(self, _reference)
		if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD then
		
			local point
			if ATME.isPoint(_reference) ~= POINT_BAD then
				point = ATME.convertToPoint3D(_reference)
			else
				point = _reference:getPosition()
			end
			
			local minDistance, maxDistance, minHDistance, maxHDistance, minDeltaAltitude, maxDeltaAltitude = C_Group_evalDistances(self, point)
			
			return minDistance
		else
			errorOutputCore(nil, "Function ATME.C_Group.getMinDistance : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
		end
	end
	
	function newGroup.getMaxDistance(self, _reference)
		if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD then
				
			local point
			if ATME.isPoint(_reference) ~= POINT_BAD then
				point = ATME.convertToPoint3D(_reference)
			else
				point = _reference:getPosition()
			end
			
			local minDistance, maxDistance, minHDistance, maxHDistance, minDeltaAltitude, maxDeltaAltitude = C_Group_evalDistances(self, point)
			
			return maxDistance
		else
			errorOutputCore(nil, "Function ATME.C_Group.getMaxDistance : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
		end
	end
	
	function newGroup.getMinHDistance(self, _reference)
		if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD then
		
			local point
			if ATME.isPoint(_reference) ~= POINT_BAD then
				point = ATME.convertToPoint3D(_reference)
			else
				point = _reference:getPosition()
			end
			
			local minDistance, maxDistance, minHDistance, maxHDistance, minDeltaAltitude, maxDeltaAltitude = C_Group_evalDistances(self, point)
			
			return minHDistance
		else
			errorOutputCore(nil, "Function ATME.C_Group.getMinHDistance : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
		end
	end
	
	function newGroup.getMaxHDistance(self, _reference)
		if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD then
				
			local point
			if ATME.isPoint(_reference) ~= POINT_BAD then
				point = ATME.convertToPoint3D(_reference)
			else
				point = _reference:getPosition()
			end
			
			local minDistance, maxDistance, minHDistance, maxHDistance, minDeltaAltitude, maxDeltaAltitude = C_Group_evalDistances(self, point)
			
			return maxHDistance
		else
			errorOutputCore(nil, "Function ATME.C_Group.getMaxHDistance : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
		end
	end
	
	function newGroup.getBarycentre(self)
		local barycentre = {
			x = 0,
			y = 0,
			z = 0,
		}
		
		local units = self:getUnits()	
		for _, _unit in pairs(units) do
			local pos = _unit:getPosition()
			
			barycentre.x = barycentre.x + pos.x
			barycentre.z = barycentre.z + pos.z
		end
		
		barycentre.x = barycentre.x/self:getNbUnits()
		barycentre.z = barycentre.z/self:getNbUnits()
		barycentre.y = ATME.getSurfaceAltitude({x = barycentre.x, y = barycentre.z})
		
		return barycentre
	end
	
	function newGroup.disable(self)
		if groupsOnBoard[self:getName()] == nil then
			-- Les groupes déchargés d'un autre véhicules ne génèrent pas d'appel 
			-- à onDisableGroupHandler
			
			for _, _localModuleMembers in C_Module_MT:pairs() do
				if _localModuleMembers.handlers.onDisableGroupHandler ~= nil then
					-- Appel du module
					outputCore(nil,"Appel onDisableGroupHandler du module " .. _localModuleMembers.handlers.name, 2)
					_localModuleMembers.handlers.onDisableGroupHandler(self)
					outputCore(nil,"Retour onDisableGroupHandler du module " .. _localModuleMembers.name, 2)
				end
			end
		end
		
		local localMembers = C_Group_MT:get(index)
		localMembers.isEnabled = false
		
		group:destroy()

		C_Group_delete(self)
	end
	
	function newGroup.setSignal(self, _radius, _signalType)
		if type(_radius) == "number" and type(_signalType) == "string" then
			if _radius > 0 then
				local signalType = string.upper(_signalType)
				local signalTypeId = string.sub(signalType, 1, 12)
				if signalTypeId == "SIGNAL_SMOKE" or signalTypeId == "SIGNAL_FLARE" then
					local signalColor = string.match(string.sub(signalType, 13), "%a+", 1)
					if (signalTypeId == "SIGNAL_SMOKE" and smokeColors[signalColor] ~= nil) or
						(signalTypeId == "SIGNAL_FLARE" and flareColors[signalColor] ~= nil) then
						local localMembers = C_Group_MT:get(index)
						
						local item = {
							unitName = "",
							groupName = self:getName(),
							standby = false,
							radius = _radius,
							type = signalTypeId,
							color = signalColor,
							timeStart = -1,
							timeSmoke = -1,
							offsetTime = 0,
						}
						
						localMembers.signalIndex = signals:add(item)
					else
						errorOutputCore(nil, "Function ATME.C_Group.setSignal : signalType format bad color")
					end
				else
					errorOutputCore(nil, "Function ATME.C_Group.setSignal : signalType format bad type")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Group.setSignal : radius must be > 0")
			end
		else
			if type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Group.setSignal : radius type must be number")
			elseif type(_signalType) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Group.setSignal : signalType type must be string")
			end
		end
	end
	
	function newGroup.resetSignal(self)
		local localMembers = C_Group_MT:get(index)
		
		if localMembers.signalIndex > 0 then
			signals:remove(localMembers.signalIndex)
			localMembers.signalIndex = 0
		end
	end
	
	function newGroup.isSignalSet(self)
		local localMembers = C_Group_MT:get(index)
		return localMembers.signalIndex ~= 0
	end
	
	function newGroup.getFirstUnit(self)
		local localMembers = C_Group_MT:get(index)
		localMembers.nextUnit = 2
		localMembers.previousUnit = 0
		return self:getUnitByName(localMembers.ATMEUnitNames[1])
	end

	function newGroup.getNextUnit(self)
		local localMembers = C_Group_MT:get(index)
		
		if localMembers.nextUnit <= #localMembers.ATMEUnitNames then
			localMembers.previousUnit = localMembers.nextUnit - 1
			localMembers.nextUnit = localMembers.nextUnit + 1

			return self:getUnitByName(localMembers.ATMEUnitNames[localMembers.nextUnit - 1])
		end
	end

	function newGroup.getPreviousUnit(self)
		local localMembers = C_Group_MT:get(index)
		
		if localMembers.previousUnit > 0 then
			localMembers.nextUnit = localMembers.previousUnit + 1
			localMembers.previousUnit = localMembers.previousUnit - 1

			return self:getUnitByName(localMembers.ATMEUnitNames[localMembers.previousUnit + 1])
		end
	end

	function newGroup.getLastUnit(self)
		local localMembers = C_Group_MT:get(index)
		localMembers.nextUnit = #localMembers.ATMEUnitNames + 1
		localMembers.previousUnit = #localMembers.ATMEUnitNames - 1
		return self:getUnitByName(localMembers.ATMEUnitNames[#localMembers.ATMEUnitNames])
	end


	function newGroup.getUnitByName(self, _name)
		if type(_name) == "string" then
			local localMembers = C_Group_MT:get(index)
			return localMembers.ATMEUnits[_name]
		else
			errorOutputCore(nil, "Function ATME.C_Group.getUnitByName : name type must be string")
		end	
	end

	function newGroup.getUnits(self)
		local localMembers = C_Group_MT:get(index)
		return localMembers.ATMEUnits
	end

	function newGroup.getNbUnits(self)
		local localMembers = C_Group_MT:get(index)
		return #localMembers.ATMEUnitNames
	end

	function newGroup.isOnlyInfantry(self)
		local localMembers = C_Group_MT:get(index)
		return localMembers.onlyInfantry
	end

	function newGroup.isOnlyAI(self)
		local localMembers = C_Group_MT:get(index)
		return localMembers.hasPlayers == false
	end

	function newGroup.isInDCSZone(self, _zoneName, _allGroup)
		if type(_zoneName) == "string" and type(_allGroup) == "boolean" then
			local zone = ATME.getDCSZone(_zoneName)
			if zone ~= nil then
				if _allGroup == false then
					return self:getMinHDistance(zone.point) <= zone.radius
				else
					return self:getMaxHDistance(zone.point) <= zone.radius
				end
			else
				errorOutputCore(nil, "Function ATME.C_Group.isInDCSZone : DCS zone " .. _zoneName .. " unknown")
			end
		else
			if type(_zoneName) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Group.isInDCSZone : zoneName type must be string")
			elseif type(_allGroup) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Group.isInDCSZone : allGroup type must be boolean")
			end
		end
	end

	function newGroup.isInZone2D(self, _reference, _radius, _allGroup)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) == true or
				ATME.isObjectClass(_reference, ATME.C_StaticObject) == true or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" and type(_allGroup) == "boolean" then
			
			if _radius > 0 then
				if _allGroup == false then
					return self:getMinHDistance(_reference) <= _radius
				else
					return self:getMaxHDistance(_reference) <= _radius
				end
			else
				errorOutputCore(nil, "Function ATME.C_Group.isInZone2D : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Group.isInZone2D : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Group.isInZone2D : radius type must be number")
			elseif type(_allGroup) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Group.isInZone2D : allGroup type must be boolean")
			end
		end
	end	
			
	function newGroup.isInZone3D(self, _reference, _radius, _allGroup)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" then
				
			if _radius > 0 then
				if _allGroup == false then
					return self:getMinDistance(_reference) <= _radius
				else
					return self:getMaxDistance(_reference) <= _radius
				end
			else
				errorOutputCore(nil, "Function ATME.C_Group.isInZone3D : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Group.isInZone3D : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Group.isInZone3D : radius type must be number")
			elseif type(_allGroup) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Group.isInZone3D : allGroup type must be boolean")
			end
		end
	end
	
	function newGroup.isNear(self, _reference, _radius, _deltaAltitude, _allGroup)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" and type(_deltaAltitude) == "number" then
				
			if _radius > 0 then
				if _deltaAltitude > 0 then
					local point
					if ATME.isPoint(_reference) ~= POINT_BAD then
						point = ATME.convertToPoint3D(_reference)
					else
						point = _reference:getPosition()
					end
					
					local minDistance, maxDistance, minHDistance, maxHDistance, minDeltaAltitude, maxDeltaAltitude = C_Group_evalDistances(self, point)
					
					if _allGroup == false then
						return minHDistance <= _radius and minDeltaAltitude <= _deltaAltitude
					else
						return maxHDistance <= _radius and maxDeltaAltitude <= _deltaAltitude
					end
				else
					errorOutputCore(nil, "Function ATME.C_Group.isNear : deltaAltitude must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Group.isNear : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Group.isNear : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Group.isNear : radius type must be number")
			elseif type(_deltaAltitude) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Group.isNear : deltaAltitude type must be number")
			elseif type(_allGroup) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Group.isNear : allGroup type must be boolean")
			end
		end
	end

	function newGroup.setPickupZone(self, _zoneName, _random)
		if type(_zoneName) == "string" and type(_random) == "number" then
			if self:isOnlyInfantry() == true then
				if ATME.getDCSZone(_zoneName) ~= nil then
					if _random > 0 and _random <= 1 then
						local localMembers = C_Group_MT:get(index)
						
						if pickupZones[_zoneName] == nil then
							pickupZones[_zoneName] = {
								groups = ATME.C_IndexList(),
							}
						end
						
						local item = {
							randomMax = _random,
							groupName = self:getName(),
							isOk = false,
							isInZone = false,
						}
						
						pickupZones[_zoneName].groups:add(item)
						localMembers.pickupZoneName = _zoneName
					else
						errorOutputCore(nil, "Function ATME.C_Group.setPickupZone : random must be > 0 and <= 1")
					end
				else
					errorOutputCore(nil, "Function ATME.C_Group.setPickupZone : DCS zone " .. _zoneName .. " unknown")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Group.setPickupZone : Not only infantry group")
			end
		else
			if type(_zoneName) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Group.setPickupZone : zoneName type must be string")
			elseif type(_random) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Group.setPickupZone : random type must be string")
			end
		end
	end

	function newGroup.resetPickupZone(self)
		local localMembers = C_Group_MT:get(index)
		
		pickupZones[localMembers.pickupZoneName].groups:remove(localMembers.pickupZoneIndex)
		
		if pickupZones[localMembers.pickupZoneName].groups:getCount() == 0 then
			pickupZones[localMembers.pickupZoneName].groups = nil
			pickupZones[localMembers.pickupZoneName] = nil
		end
		
		localMembers.pickupZoneName = ""
		localMembers.pickupZoneIndex = 0
	end

	function newGroup.hasPickupZone(self)
		local localMembers = C_Group_MT:get(index)
		
		return localMembers.pickupZoneName ~= ""
	end

	function newGroup.getPickupZoneName(self)
		local localMembers = C_Group_MT:get(index)
		
		if localMembers.pickupZoneName ~= "" then
			return localMembers.pickupZoneName
		end
	end

	function newGroup.isReadyToBoard(self)
		return readyToBoardGroups[self:getName()] ~= nil
	end

	function newGroup.changeReadyToBoard(self, _value)
		if type(_value) == "boolean" then
			if self:isOnlyInfantry() == true then
				if _value == true and readyToBoardGroups[self:getName()] == nil then
					readyToBoardGroups[self:getName()] = self
					self:stop()
				elseif _value == false and self:isBoardingStarted() == false and readyToBoardGroups[self:getName()] ~= nil then
					self:move()
					readyToBoardGroups[self:getName()] = nil
				end
			end
		else
			errorOutputCore(nil, "Function ATME.C_Group.changeReadyToBoard : value type must be boolean")
		end	
	end

	function newGroup.isBoardingStarted(self)
		local localMembers = C_Group_MT:get(index)
		return localMembers.boardingStarted
	end

	function newGroup.move(self)
		local localMembers = C_Group_MT:get(index)
		
		if localMembers.isStopped == true then
			local group = self:getDCSGroup()
			trigger.action.groupContinueMoving(group)
			localMembers.isStopped = false
		end
	end

	function newGroup.stop(self)
		local localMembers = C_Group_MT:get(index)
		
		if localMembers.isStopped == false then
			local group = self:getDCSGroup()
			trigger.action.groupStopMoving(group)
			localMembers.isStopped = true
		end
	end

	function newGroup.isStopped(self)
		local localMembers = C_Group_MT:get(index)
		return localMembers.isStopped
	end

	function newGroup.freeze(self, _freezeOn)
		if type(_freezeOn) == "boolean" then
			local group = self:getDCSGroup()
			if _freezeOn == true then
				trigger.action.setGroupAIOff(group)
			else
				trigger.action.setGroupAIOn(group)
			end
		else
			errorOutputCore(nil, "Function ATME.C_Group.freeze : freezeOn type must be boolean")
		end	
	end
	
	
	C_Group_addUnit(newGroup, _ATMEUnit)
	groups[name] = newGroup
	
	newGroup.modules = {}
	setmetatable(newGroup.modules, C_Group_MT.mtModules)
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if newGroup.modules[_localModuleMembers.name] == nil then
			newGroup.modules[_localModuleMembers.name] = {}
		end
		
		if groupsOnBoard[newGroup:getName()] == nil then
			-- Les groupes déchargés d'un autre véhicules ne génèrent pas d'appel 
			-- à onCreateGroupHandler
			
			if _localModuleMembers.handlers.onCreateGroupHandler ~= nil then
				-- Appel du module
				outputCore(nil,"Appel onCreateGroupHandler du module " .. _localModuleMembers.name, 2)
				_localModuleMembers.handlers.onCreateGroupHandler(newGroup)
				outputCore(nil,"Retour onCreateGroupHandler du module " .. _localModuleMembers.name, 2)
			end
		end
	end
	
	outputCore(nil,"Create group = " .. name, 2)
end

C_Group_delete = function(_ATMEGroup)
	local localMembers = C_Group_MT:get(_ATMEGroup:getIndex())
	if #localMembers.ATMEUnitNames == 0 then
		if localMembers.signalIndex > 0 then
			local signal = signals:get(localMembers.signalIndex)
			if localMembers.pickupZoneName ~= "" and signal.standby == false then
				for _, _group in pickupZones[localMembers.pickupZoneName].groups:pairs() do
					local group = ATME.C_Group.getByName(_group.groupName)
					if group ~= nil then
						local currentLocalMembers = C_Group_MT:get(group:getIndex())
						if currentLocalMembers.signalIndex > 0 then
							local currentSignal = signals:get(currentLocalMembers.signalIndex)
							currentSignal.standby = false
						end
					end
				end
			end
			
						
			signals:remove(localMembers.signalIndex)
			localMembers.signalIndex = 0
		end
		
		C_Group_MT:remove(_ATMEGroup:getIndex())
		for _, _localModuleMembers in C_Module_MT:pairs() do
			if groupsOnBoard[_ATMEGroup:getName()] == nil then
				-- Les groupes déchargés d'un autre véhicules ne génèrent pas d'appel 
				-- à onDeleteGroupHandler
				
				if _localModuleMembers.handlers.onDeleteGroupHandler ~= nil then
					-- Appel du module
					outputCore(nil,"Appel onDeleteGroupHandler du module " .. _localModuleMembers.name, 2)
					_localModuleMembers.handlers.onDeleteGroupHandler(_ATMEGroup, localMembers.isEnabled)
					outputCore(nil,"Retour onDeleteGroupHandler du module " .. _localModuleMembers.name, 2)
				end
			end
			
			_ATMEGroup.modules[_localModuleMembers.name] = nil
		end
		
		

		
		for _name, _pickupZone in pairs(pickupZones) do
			for _index, _group in _pickupZone.groups:pairs() do
				if _group.groupName == _ATMEGroup:getName() then
					_pickupZone.groups:remove(_index)
				end
			end
			
			if _pickupZone.groups:getCount() == 0 then
				pickupZones[_name].groups = nil
				pickupZones[_name] = nil
			end
		end
		
		groups[_ATMEGroup:getName()] = nil
		readyToBoardGroups[_ATMEGroup:getName()] = nil
		
		outputCore(nil,"Delete group = " .. _ATMEGroup:getName(), 2)
	else
		local nb = #localMembers.ATMEUnitNames
		while nb > 0 do
			C_AIUnit_delete(_ATMEGroup:getUnitByName(localMembers.ATMEUnitNames[1]))
			nb = nb - 1
		end
	end
end

setmetatable(ATME.C_Group,C_Group_MT)

C_Group_MT.mt.__metatable = "Private"
C_Group_MT.mt.__index = ATME.C_Group
C_Group_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Group_MT.keys) do
		if k == key then
			found = true
		end
	end

	if found == false then
		for _, _localModuleMembers in C_Module_MT:pairs() do

			if _localModuleMembers.name == k then
				found = true
			end
		end
		
		if found == false then
			errorOutputCore(nil, "Function ATME.C_Group : Bad key " .. k)
		end
	end
	
	if found == true then
		rawset(t, k, v)
	end
end

C_Group_MT.mtModules.__metatable = "Private"
C_Group_MT.mtModules.__newindex = function(t, k, v)
	local found = false
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if _localModuleMembers.name == k then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil,"ATME : ATME.C_Group.modules Bad key " .. k)
	end
end


-- Fonctions communes à ATME.C_Player et ATME.C_AIUnit

local function C_Player_C_AIUnit_unload(_transportUnit, _id)
	local localMembers 
	if _transportUnit:getClassName() == "C_Player" then
		localMembers = C_Player_MT:get(_transportUnit:getIndex())
	else
		localMembers = C_AIUnit_MT:get(_transportUnit:getIndex())
	end
	
	local id = math.floor(_id)
	local unloadGroupDatas = localMembers.groupsDatasOnBoard:get(id)
	
	if unloadGroupDatas ~= nil then
		if _transportUnit:isGroundVehicle() or ((_transportUnit:isHelicopter() or _transportUnit:isPlane()) and _transportUnit:inAir() == false) then
			if _transportUnit:getHSpeed() < 3 then
				local localGroupMembers = C_GroupSpawnDatas_MT:get(unloadGroupDatas:getIndex())

				if ATME.C_Group.exists(localGroupMembers.datas.name) == false then
					local index = 0
					for _i, _unit in ipairs(localGroupMembers.datas.units) do
						if ATME.C_AIUnit.exists(_unit.name) == true then
							index = _i
						end
					end
					
					if index == 0 then
						local nbUnitsToUnload = #localGroupMembers.datas.units
						local err, spawnedGroup, nbDeadUnits = C_SpawnGroup_spawnFromObject(unloadGroupDatas, _transportUnit)
						
						if err == true then
							return true, spawnedGroup, nbDeadUnits
						end
						
						-- ATTENTION : Lors du spawn, les unités sont crées par l'événement DCS S_EVENT_BIRTH
						-- En sortant dans la fonction, les infos ATME pour le groupe et les unités sont à jour.
						-- Donc la mise à jour de groupsOnBoard, se fait ici.
						
						groupsOnBoard[spawnedGroup:getName()] = nil
						
						localMembers.nbUnitsOnBoard = localMembers.nbUnitsOnBoard - nbUnitsToUnload
						localMembers.groupsDatasOnBoard:remove(id)
						
						local event = {
							type = "TRANSPORT_END_OF_DISEMBARK",
							datas = {
								unitName = _transportUnit:getName(),
								index = id,
								linkedGroupName = spawnedGroup:getName(),
								nbUnitsOnBoard = localMembers.nbUnitsOnBoard,
								nbDeadUnits = nbDeadUnits,
							},
						}
							
						C_EventsMgr_addCoreEvent(event)
						
						return false, spawnedGroup, nbDeadUnits
					else
						return true, "UNLOAD_INFANTRY_UNIT_ALREADY_EXIST", localGroupMembers.datas.units[index].name
					end
				else
					return true, "UNLOAD_GROUP_EXISTS", localGroupMembers.datas.name
				end
			else
				return true, "LOAD_UNIT_BAD_SPEED"
			end
		else
			return true, "UNLOAD_UNIT_IN_AIR"
		end
	else
		return true, "UNLOAD_BAD_ID"
	end
end

local function C_Player_C_AIUnit_load(_transportUnit, _groupToBoard, _radius)
	local maxUnits = personnelCarrierAttributes[_transportUnit:getTypeName()].maxUnits
	local nbUnits
	local nbUnitsBoarding = 0
	
	local localMembers 
	if _transportUnit:getClassName() == "C_Player" then
		localMembers = C_Player_MT:get(_transportUnit:getIndex())
	else
		localMembers = C_AIUnit_MT:get(_transportUnit:getIndex())
	end
	
	nbUnits = localMembers.nbUnitsOnBoard
								
	for _, _groupItem in ipairs(localMembers.groupsBoarding) do
		nbUnitsBoarding = nbUnitsBoarding + _groupItem.nbUnits
	end
	
	if _groupToBoard:getNbUnits() + nbUnits + nbUnitsBoarding <= maxUnits then
		if _transportUnit:isGroundVehicle() or ((_transportUnit:isHelicopter() or _transportUnit:isPlane()) and _transportUnit:inAir() == false) then
			if _transportUnit:getHSpeed() < 3 then
				local transportUnitPosition = _transportUnit:getPosition()
				if _groupToBoard:getMinHDistance(transportUnitPosition) <= _radius then	
					local barycentre = _groupToBoard:getBarycentre()
					
					local v = ATME.C_Vector3D(barycentre, transportUnitPosition)
					local v1 = v:toUnitVector()

					local waypoints = {}
					waypoints[1] = {
						action = "Off Road",
						x = transportUnitPosition.x,
						y = transportUnitPosition.z,
						type = "Turning Point",
						speed = 5,
					}
					
					waypoints[2] = {
						action = "Off Road",
						x = transportUnitPosition.x + v1:getX() * 500,
						y = transportUnitPosition.z + v1:getZ() * 500,
						type = "Turning Point",
						speed = 5,
					}
					
					local line = ATME.C_Line2D(transportUnitPosition, barycentre)
					local axis = line:getPerpendicular(0)

					C_GroupSpawnDatas_setWaypoints(_groupToBoard, waypoints)
					_groupToBoard:move()
					
					local item = {
						groupName = _groupToBoard:getName(),
						nbUnits = _groupToBoard:getNbUnits(),
					}
					table.insert(localMembers.groupsBoarding, item)
					
					local pA = axis:getOrigin()
					local pB = axis:getPointFromOrigin(10)
					
					local localGroupMembers = C_Group_MT:get(_groupToBoard:getIndex())
					localGroupMembers.boardingStarted = true
					local item = {
						activeGroupName = _groupToBoard:getName(),
						origineMissionDatasGroupName = localGroupMembers.origineMissionDatasGroupName,
						waypointsGroupName = localGroupMembers.waypointsGroupName,
						transportUnitName = _transportUnit:getName(),
						unitsOnBoard = {},
						unitsLost = {},
						timeStart = ATME.getTime(),
						initialDistance = _groupToBoard:getMaxHDistance(transportUnitPosition),
						A = pA,
						B = pB,
						sideOfWP2 = _transportUnit:whichSide(waypoints[2]),
					}
					
					groupsToBoard:add(item)
					
					return false
				else
					return true, "LOAD_UNIT_TOO_FAR"
				end
			else
				return true, "LOAD_UNIT_BAD_SPEED"
			end
		else
			return true, "LOAD_UNIT_IN_AIR"
		end
	else
		return true, "LOAD_TOO_MUCH_INFANTRY"
	end
end

local function C_Player_C_AIUnit_getNearestReadyToBoardGroups(_transportUnit, _radius)
	local unitPosition = _transportUnit:getPosition()
	local radius = math.floor(_radius)
	local minDistance = -1
	local nearestGroup
	
	for _, _group in pairs(readyToBoardGroups) do
		local distance = _group:getMinHDistance(unitPosition)
		
		if distance <= radius and _group:getCoalitionName() == _transportUnit:getCoalitionName() and 
				_group:isBoardingStarted() == false and _group:isActivated() == true then
			if minDistance < 0 or distance < minDistance then
				minDistance = distance
				nearestGroup = _group
			end
		end
	end
	
	return nearestGroup
end


local function C_Player_C_AIUnit_addOnBoardGroup(_transportUnit, _groupDatas)
	
	local localMembers 
	if _transportUnit:getClassName() == "C_Player" then
		localMembers = C_Player_MT:get(_transportUnit:getIndex())
	else
		localMembers = C_AIUnit_MT:get(_transportUnit:getIndex())
	end
	
	
	localMembers.groupsDatasOnBoard:add(_groupDatas)
	groupsOnBoard[_groupDatas:getName()] = _transportUnit
	
	return localMembers.groupsDatasOnBoard:getCount()
end

----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Player : gère les unités joueurs au format ATME. Pas de contructeur accessible
-- pour les modules externes.
-- Les instances de cette classe sont intégralement gérées par le module ATME_Core. La création et la destruction
-- dépendent du contexte de début de mission et des événements qui se produisent dans DCS en cours de mission.
-- Les modules externes accèdent à cette gestion indirectement par les handlers définis dans ceux-ci et les méthodes
-- accessibles de cette classe. Ceci permet de garantir une cohérence sur les listes internes ATME.
----------------------------------------------------------------------------------------------------------

-- C_Player_MT prédéclarée avant en local
C_Player_MT = {
	className = "C_Player",
	
	mtModules = {},
	mt = {},
	
	keys = {
		"isEngineStarted", "getIndex", "getDCSUnit", "getName", "getTypeName", "getF10MenuRoot", "getNbUnitsOnBoard", "getGroupsNameOnBoard",
		"getPseudo", "getGroup", "getGroupId", "display", "soundOnce", "soundStart", "soundStartPlayList", "soundChange", "load", "unload",
		"soundChangePlayList", "soundPause", "soundStop", "getFuelRatio", "getLife", "getLifeRatio", "getCallsign", "getNearestReadyToBoardGroups",
		"getPosition", "getVelocityVector", "getSpeed", "getHSpeed", "getVSpeed", "getRollAxisVector", "getSpeedAzimuth", "getAzimuth", 
		"getCountryName", "getCoalitionName", "isHelicopter", "isPersonnelCarrier", "isPlane", "inAir", "isGroundVehicle", "getMSLAltitude", "getAGLAltitude", 
		"explode", "isInDCSZone", "isInZone2D", "isInZone3D", "isNear", "whichSide", "crossAxisFromRightToLeft", "crossAxisFromLeftToRight", 
		"isRouteInDirection", "modules",
	},
	
	localMembers = ATME.C_IndexList(),
	
	add = function(self, _player)
		local item = {
			engineOn = false,
			nbUnitsOnBoard = 0,
			groupsBoarding = {},
			groupsDatasOnBoard = ATME.C_IndexList(),
			playList = {},
			currentReadIndex = 1,
			randomRead = false,
			soundStartTime = 0,
			soundInterval = 0,
			soundIntervalNext = 0,
			pauseStartTime = 0,
			pauseDuration = 0,
			itemF10MenuCommandRoot = nil,
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:get(_id).itemF10MenuCommandRoot:removeAll()
		C_F10Menu_MT:remove(self.localMembers:get(_id).itemF10MenuCommandRoot:getIndex())
		self.localMembers:get(_id).itemF10MenuCommandRoot = nil
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

ATME.C_Player = {
	getClassName = function(self)
		return C_Player_MT.className
	end,
	
	exists = function(_playerName)
		if type(_playerName) == "string" then
			return players[_playerName] ~= nil
		else
			errorOutputCore(nil, "Function ATME.C_Player.exists : name must be string")
		end
	end,
	
	getByName = function(_playerName)
		if type(_playerName) == "string" then
			return players[_playerName]
		else
			errorOutputCore(nil, "Function ATME.C_Player.exists : name must be string")
		end
	end,
}


local function C_Player_create(_unit)
	local newPlayer = {}
	setmetatable(newPlayer, C_Player_MT.mt)
	
	local name = _unit:getName()
	
	local unit = _unit
	local typeName = _unit:getTypeName()
	local groupId = allPlayerUnitsInMission[name].group.groupId
	local waypoints = allPlayerUnitsInMission[name].group.waypoints
	local entry = 0				
	local airbase
	local index = 0
	local oldSide
	
	local groupName = _unit:getGroup():getName()
	local ATMEGroup
	
	
	function newPlayer.getIndex(self)
		return index
	end
	
	function newPlayer.getGroupId(self)
		return groupId
		
		-- Bug DCS getID
		--local group = ATMEGroup:getDCSGroup()
		--return group:getID()
	end
	
	function newPlayer.getDCSUnit(self)
		return unit
	end
	
	function newPlayer.getName(self)
		return name
	end
	
	function newPlayer.getPseudo(self)
		return unit:getPlayerName()
	end
	
	function newPlayer.getTypeName(self)
		return typeName
	end
	
	function newPlayer.getGroup(self)
		return ATMEGroup
	end
	
	function newPlayer.display(self, _text, _duration)
		if type(_text) == "string" and type(_duration) == "number" then
			if _duration >= 0 then
				trigger.action.outTextForGroup(groupId, _text, _duration)
			else
				errorOutputCore(nil, "Function ATME.C_Player.display : duration type must be >= 0")
			end
		else
			if type(_text) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Player.display : text type must be string")
			elseif type(_duration) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.display : duration type must be number")
			end
		end
	end

	function newPlayer.soundOnce(self, _file)
		if type(_file) == "string" then
			trigger.action.outSoundForGroup(groupId, _file)
		else
			errorOutputCore(nil, "Function ATME.C_Player.soundOnce : file must be string")
		end
	end
	
	function newPlayer.soundStartPlayList(self, _playList, _randomRead, _interval)
		if type(_playList) == "table" and type(_randomRead) == "boolean" and type(_interval) == "number" then
			if #_playList > 0 then
				if _interval >= 1 then
					local ok = false
					local localMembers = C_Player_MT:get(index)
					local playList = {}
					
					for _, _file in ipairs(_playList) do
						if type(_file) == "string" then
							ok = true
							table.insert(playList, _file)
						end
					end
					
					if ok == true then
						if entry ~= 0 then
							self:soundStop()
							entry = 0
						end
						
						if entry == 0 then
							localMembers.soundInterval = math.floor(_interval)
							localMembers.soundStartTime = ATME.getTime()
							localMembers.randomRead = _randomRead
							localMembers.playList = playList
							localMembers.currentReadIndex = 1
							entry = activatedSounds:add(self)
							
							if _randomRead == true then
								localMembers.currentReadIndex = math.random(1, #localMembers.playList)
							end
							
							trigger.action.outSoundForGroup(groupId, localMembers.playList[localMembers.currentReadIndex])
						end
					else
						errorOutputCore(nil, "Function ATME.C_Player.soundStartPlayList : file type in playList must be string")
					end
				else
					errorOutputCore(nil, "Function ATME.C_Player.soundStartPlayList : interval must be >= 1")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Player.soundStartPlayList : playList is empty")
			end
		else
			if type(_playList) ~= "table" then
				errorOutputCore(nil, "Function ATME.C_Player.soundStartPlayList : playList type must be table")
			elseif type(_randomRead) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Player.soundStartPlayList : randomRead type must be boolean")
			elseif type(_interval) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.soundStartPlayList : interval type must be number")
			end
		end
	end
	
	function newPlayer.soundChangePlayList(self, _playList, _interval, _forced)
		if type(_playList) == "table" and type(_interval) == "number" and type(_forced) == "boolean" then
			if #_playList > 0  then
				if _interval >= 1 then
					if entry ~= 0 then
						local ok = false
						local localMembers = C_Player_MT:get(index)
						local playList = {}
						
						for _, _file in ipairs(_playList) do
							if type(_file) == "string" then
								ok = true
								table.insert(playList, _file)
							end
						end
						
						if ok == true then
							local localMembers = C_Player_MT:get(index)
							if _forced == true then
								localMembers.soundInterval = math.floor(_interval)
								self:soundStartPlayList(playList, localMembers.randomRead, localMembers.soundInterval)
							else
								localMembers.soundIntervalNext = math.floor(_interval)
								localMembers.playList = playList
							end
						else
							errorOutputCore(nil, "Function ATME.C_Player.soundChangePlayList : file type in playList must be string")
						end
					end
				else
					errorOutputCore(nil, "Function ATME.C_Player.soundChangePlayList : interval must be >= 1")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Player.soundChangePlayList : playList is empty")
			end
		else
			if type(_playList) ~= "table" then
				errorOutputCore(nil, "Function ATME.C_Player.soundChangePlayList : playList type must be table")
			elseif type(_interval) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.soundChangePlayList : interval type must be number")
			elseif type(_forced) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Player.soundChangePlayList : forced type must be boolean")
			end
		end
	end
	
	function newPlayer.soundStart(self, _file, _interval)
		if type(_file) == "string" and type(_interval) == "number" then
			if _interval >= 1 then
				local localMembers = C_Player_MT:get(index)
		
				if entry ~= 0 then
					self:soundStop()
					entry = 0
				end
				
				if entry == 0 then
					local playList = {_file}
					self:soundStartPlayList(playList, false, _interval)
				end
			else
				errorOutputCore(nil, "Function ATME.C_Player.soundStart : interval must be >= 1")
			end
		else
			if type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Player.soundStart : file type must be string")
			elseif type(_interval) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.soundStart : interval type must be number")
			end
		end
	end
	
	function newPlayer.soundChange(self, _file, _interval, _forced)
		if type(_file) == "string" and type(_forced) == "boolean" then
			if _interval >= 1 then
				if entry ~= 0 then
					local localMembers = C_Player_MT:get(index)
					local playList = {_file}

					if _forced == true then
						self:soundStartPlayList(playList, false, _interval)
					else
						self:soundChangePlayList(self, playList, _interval, _forced)
					end
				end
			else
				errorOutputCore(nil, "Function ATME.C_Player.soundChange : interval must be >= 1")
			end
		else
			if type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_Player.soundChange : file type must be string")
			elseif type(_interval) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.soundChange : interval type must be number")
			elseif type(_forced) ~= "boolean" then
				errorOutputCore(nil, "Function ATME.C_Player.soundChange : forced type must be boolean")
			end
		end
	end
	
	function newPlayer.soundPause(self, _duration)
		if type(_duration) == "number" then
			if _duration > 0 then
				local duration = math.floor(_duration)
				if entry ~= 0 then
					local localMembers = C_Player_MT:get(index)
					localMembers.pauseDuration = duration
					localMembers.pauseStartTime = ATME.getTime()
				end
			end
		else
			errorOutputCore(nil, "Function ATME.C_Player.soundPause : duration type must be number")
		end
	end
	
	function newPlayer.soundStop(self)
		if entry ~= 0 then
			local localMembers = C_Player_MT:get(index)
			activatedSounds:remove(entry)
			entry = 0
		end
	end
	
	function newPlayer.getFuelRatio(self)
		return unit:getFuel()
	end
	
	function newPlayer.getLife(self)
		return unit:getLife()
	end
	
	function newPlayer.getLifeRatio(self)
		return self:getLife()/unit:getLife0()
	end
	
	function newPlayer.getCallsign(self)
		local callsign = unit:getCallsign()
		local name = string.match(callsign, "^%a*")
		local id1 = string.match(callsign, "%d")
		local id2 = string.match(callsign, "%d$")
		
		return name, id1, id2
	end
	
	function newPlayer.getPosition(self)
		return unit:getPosition().p
	end

	function newPlayer.getVelocityVector(self)
		local vector = ATME.C_Vector3D(unit:getVelocity())
		return vector
	end

	function newPlayer.getSpeed(self)
		local vector = self:getVelocityVector()
		return vector:getModule(vector)
	end

	function newPlayer.getHSpeed(self)
		local vector = self:getVelocityVector()
		return vector:getHModule(vector)
	end

	function newPlayer.getVSpeed(self)
		local vector = unit:getVelocity()
		return vector.y
	end

	function newPlayer.getSpeedAzimuth(self)
		local vector = self:getVelocityVector()
		
		if vector:getModule() > 1 then 
			return vector:getAzimuth()
		else
			return 0
		end
	end
	
	function newPlayer.getRollAxisVector(self)
		return ATME.C_Vector3D(unit:getPosition().x)
	end

	function newPlayer.getAzimuth(self)
		local vector = self:getRollAxisVector()
		return vector:getAzimuth()
	end

	function newPlayer.getCountryName(self)
		local id = unit:getCountry()
		
		return country.name[id]
	end

	function newPlayer.getCoalitionName(self)
		local id = unit:getCoalition()
		return coalitionNames[id]
	end

	function newPlayer.isHelicopter(self)
		return unit:hasAttribute("Helicopters")
	end
	
	function newPlayer.isPersonnelCarrier(self)
		return personnelCarrierAttributes[self:getTypeName()] ~= nil
	end

	function newPlayer.isPlane(self)
		return unit:hasAttribute("Planes")
	end

	function newPlayer.inAir(self)
		return unit:inAir()
	end
	
	function newPlayer.isGroundVehicle(self)
		return unit:hasAttribute("Ground vehicles")
	end

	function newPlayer.getMSLAltitude(self)
		return ATME.getPointAltitude(self:getPosition())
	end

	function newPlayer.getAGLAltitude(self)
		local landHeight = ATME.getSurfaceAltitude(self:getPosition())
		return ATME.getPointAltitude(self:getPosition()) - landHeight
	end

	function newPlayer.explode(self, _power)
		if type(_power) == "number" then
			if _power >= 1 then
				ATME.explode(self:getPosition(), _power)
			else
				errorOutputCore(nil, "Function ATME.C_Player.explode : power must be >= 1")
			end
		else
			errorOutputCore(nil, "Function ATME.C_Player.explode : power type must be number")
		end
	end

	function newPlayer.isInDCSZone(self, _name)
		if type(_name) == "string" then
			local zone = ATME.getDCSZone(_name)
			
			if zone ~= nil then
				local vector = ATME.C_Vector3D(self:getPosition(), zone.point)
				if vector:getHModule() <= zone.radius then
					return true
				else
					return false
				end
			else
				errorOutputCore(nil, "Function ATME.C_Player.isInDCSZone : DCS zone " .. _name .. " unknown")
			end
		else
			errorOutputCore(nil, "Function ATME.C_Player.isInDCSZone : name type must be string")
		end
	end
	
	function newPlayer.isInZone2D(self, _reference, _radius)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) == true or
				ATME.isObjectClass(_reference, ATME.C_StaticObject) == true or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" then
			
			if _radius > 0 then
				local vector

				if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
							ATME.isObjectClass(_reference, ATME.C_StaticObject) then
					vector = ATME.C_Vector3D(self:getPosition(), _reference:getPosition())
				else
					vector = ATME.C_Vector3D(self:getPosition(), _reference)
				end
				
				return vector:getHModule() <= _radius
			else
				errorOutputCore(nil, "Function ATME.C_Player.isInZone2D : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Player.isInZone2D : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.isInZone2D : radius type must be number")
			end
		end
	end
	
	function newPlayer.isInZone3D(self, _reference, _radius)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" then
			if _radius > 0 then
				local vector

				if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or ATME.isObjectClass(_reference, ATME.C_StaticObject) then
					vector = ATME.C_Vector3D(self:getPosition(), _reference:getPosition())
				else
					vector = ATME.C_Vector3D(self:getPosition(), _reference)
				end
				
				return vector:getModule() <= _radius
			else
				errorOutputCore(nil, "Function ATME.C_Player.isInZone3D : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Player.isInZone3D : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.isInZone3D : radius type must be number")
			end
		end
	end
	
	function newPlayer.isNear(self, _reference, _radius, _deltaAltitude)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" and type(_deltaAltitude) == "number" then
				
			if _radius > 0 then
				if _deltaAltitude > 0 then
					local vector

					if ATME.isPoint(_reference) ~= POINT_BAD then
						vector = ATME.C_Vector3D(self:getPosition(), _reference)
					else
						vector = ATME.C_Vector3D(self:getPosition(), _reference:getPosition())
					end
					
					return vector:getHModule() <= _radius and math.abs(vector:getY()) <= _deltaAltitude
				else
					errorOutputCore(nil, "Function ATME.C_Player.isNear : deltaAltitude must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Player.isNear : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Player.isNear : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.isNear : radius type must be number")
			elseif type(_deltaAltitude) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.isNear : deltaAltitude type must be number")
			end
		end
	end
	
	function newPlayer.whichSide(self, _reference)
		if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD then
			local rollAxis = ATME.C_Line2D(self:getRollAxisVector(), self:getPosition())
			local A = rollAxis:getOrigin()
			local B = rollAxis:getPointFromOrigin(10)
			
			local point
			if ATME.isPoint(_reference) ~= POINT_BAD then
				point = ATME.convertToPoint3D(_reference)
			else
				point = _reference:getPosition()
			end

			return ATME.whichSide(A, B, point)
		else
			errorOutputCore(nil, "Function ATME.C_Player.whichSide : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
		end
	end
	
	function newPlayer.crossAxisFromRightToLeft(self, _A, _B)
		if ATME.isPoint(_A) ~= POINT_BAD and ATME.isPoint(_B) ~= POINT_BAD then
			local retValue
			retValue, oldSide = crossAxis(_A, _B, self:getPosition(), oldSide, -1)
			
			return retValue
		else
			if ATME.isPoint(_A) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Player.crossAxisFromRightToLeft : A type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_B) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Player.crossAxisFromRightToLeft : B type must be table (x,y) or (x,y,z)")
			end
		end
	end
	
	function newPlayer.crossAxisFromLeftToRight(self, _A, _B)
		if ATME.isPoint(_A) ~= POINT_BAD and ATME.isPoint(_B) ~= POINT_BAD then
			local retValue
			retValue, oldSide = crossAxis(_A, _B, self:getPosition(), oldSide, 1)
			
			return retValue
		else
			if ATME.isPoint(_A) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Player.crossAxisFromLeftToRight : A type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_B) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_Player.crossAxisFromLeftToRight : B type must be table (x,y) or (x,y,z)")
			end
		end
	end
	
	function newPlayer.isRouteInDirection(self, _reference)
		if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD then
			
			local point
			if ATME.isPoint(_reference) ~= POINT_BAD then
				point = ATME.convertToPoint3D(_reference)
			else
				point = _reference:getPosition()
			end
			
			return isAzimuthInLimits(self:getPosition(), self:getSpeedAzimuth(), point)
		else
			errorOutputCore(nil, "Function ATME.C_Player.isRouteInDirection : point type must be table (x,y) or (x,y,z)")
		end
	end
	
	function newPlayer.isEngineStarted(self)
		local localMembers = C_Player_MT:get(index)
		return localMembers.engineOn
	end

	function newPlayer.getF10MenuRoot(self)
		local localMembers = C_Player_MT:get(index)
		return localMembers.itemF10MenuCommandRoot
	end

	function newPlayer.getNbUnitsOnBoard(self)
		local localMembers = C_Player_MT:get(index)
		return localMembers.nbUnitsOnBoard
	end

	function newPlayer.getGroupsNameOnBoard(self)
		local localMembers = C_Player_MT:get(index)
		local temp = {}
		
		for _, _groupDatas in localMembers.groupsDatasOnBoard:pairs() do
			table.insert(temp, _groupDatas:getName())
		end
		return temp
	end

	function newPlayer.unload(self, _id)
		if type(_id) == "number" then
			local err, errInfos, errDatas = C_Player_C_AIUnit_unload(self, _id)
			
			if err == true then
				if errInfos == "UNLOAD_INFANTRY_UNIT_ALREADY_EXIST" then
					errorOutputCore(nil, "Function ATME.C_Player.unload : " .. errDatas .. " unit already exists !")
				elseif errInfos == "UNLOAD_GROUP_EXISTS" then
					errorOutputCore(nil, "Function ATME.C_Player.unload : " .. errDatas .. " group already exists !")
				else
					return true, errInfos -- errInfos = code d'erreur
				end
			else
				return false, errInfos, errDatas  --- errInfos = spawnedGroup et errDatas = nbDeadUnits
			end
		else
			errorOutputCore(nil, "Function ATME.C_Player.unload : id type must be number")
		end
	end

	function newPlayer.load(self, _groupToBoard, _radius)
		if ATME.isObjectClass(_groupToBoard, ATME.C_Group) == true  and type(_radius) == "number" then
			if _groupToBoard:isOnlyInfantry() == true then
				if _radius > 0 then
					if _groupToBoard:isReadyToBoard() == true and _groupToBoard:isBoardingStarted() == false then
						if personnelCarrierAttributes[self:getTypeName()] ~= nil then
							return C_Player_C_AIUnit_load(self, _groupToBoard, _radius)
						else
							errorOutputCore(nil, "Function ATME.C_Player.load : " .. self:getName() .. " not a transport unit")
						end
					else
						return true, "LOAD_INFANTRY_GROUP_NOT_READY"
					end
				else
					errorOutputCore(nil, "Function ATME.C_Player.load : radius must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Player.load : Not infantry group")
			end
		else
			if ATME.isObjectClass(_groupToBoard, ATME.C_Group) == false then
				if _groupToBoard ~= nil then
					errorOutputCore(nil, "Function ATME.C_Player.load : groupToBoard type must be ATME.C_Group")
				else
					return true, "LOAD_NO_INFANTRY_GROUP_AVAILABLE"
				end
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_Player.load : radius type must be number")
			end
		end
	end

	function newPlayer.getNearestReadyToBoardGroups(self, _radius)
		if type(_radius) == "number" then
			if _radius >= 0 then
				return C_Player_C_AIUnit_getNearestReadyToBoardGroups(self, _radius)
			else
				errorOutputCore(nil, "Function ATME.C_Player.getNearestReadyToBoardGroups : radius must be > 0")
			end
		else
			errorOutputCore(nil, "Function ATME.C_Player.getNearestReadyToBoardGroups : radius must be number")
		end
	end
	
	
	index = C_Player_MT:add(newPlayer)
	
	local wp = waypoints[1]
	
	local localMembers = C_Player_MT:get(newPlayer:getIndex())
	if wp.airdromeId ~= nil or wp.helipadId ~= nil then
			if wp.airdromeId ~= nil then
				airbase = airbasesByID[wp.airdromeId]:getName()
			elseif wp.helipadId ~= nil then
				airbase = helipadsAirFieldsByID[wp.helipadId]:getName()
			end
			
			if wp.type == "TakeOffParkingHot" then
				-- Cas d'initialisation en fonction de la position de début de mission
				-- Au parking moteur allumé
				
				localMembers.engineOn = true
				
			elseif wp.type == "TakeOffParking" then
				-- Cas d'initialisation en fonction de la position de début de mission
				-- Au parking moteur éteint
				
				localMembers.engineOn = false
				
			elseif wp.type == "TakeOff" then
				-- Cas d'initialisation en fonction de la position de début de mission
				-- Sur la piste, prêt à décoler
				
				localMembers.engineOn = true
			end
	else
		-- Cas d'initialisation en fonction de la position de début de mission
		-- En vol....
		
		localMembers.engineOn = true
	end
	
	
	players[name] = newPlayer
	allAIUnitsAndPlayers[name] = newPlayer
	
	if personnelCarrierAttributes[newPlayer:getTypeName()] ~= nil and newPlayer:isHelicopter() == true then
		signalCapabilityUnits[name] = newPlayer
	end
	
	if ATME.C_Group.exists(groupName) == false then
		C_Group_create(newPlayer)
	else
		C_Group_addUnit(groups[groupName], newPlayer)
	end
	
	ATMEGroup = groups[groupName]
	localMembers.itemF10MenuCommandRoot = C_F10Menu_createRoot(newPlayer)
	
	newPlayer.modules = {}
	setmetatable(newPlayer.modules, C_Player_MT.mtModules)
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if newPlayer.modules[_localModuleMembers.name] == nil then
			newPlayer.modules[_localModuleMembers.name] = {}
		end
		if _localModuleMembers.handlers.onCreatePlayerHandler ~= nil then
			-- Appel du module
			outputCore(nil,"Appel onCreatePlayerHandler du module " .. _localModuleMembers.name, 2)
			_localModuleMembers.handlers.onCreatePlayerHandler(newPlayer)
			outputCore(nil,"Retour onCreatePlayerHandler du module " .. _localModuleMembers.name, 2)
		end
	end

	outputCore(nil,"Create Player = " .. name , 2)
end

C_Player_delete = function(_player)
	local localMembers = C_Player_MT:get(_player:getIndex())
	_player:soundStop()
	
	for _, _race in pairs(races) do
		_race:removePlayer(_player)
	end

	for _, _localModuleMembers in C_Module_MT:pairs() do
		if _localModuleMembers.handlers.onDeletePlayerHandler ~= nil then
			-- Appel du module
			outputCore(nil,"Appel onDeletePlayerHandler du module " .. _localModuleMembers.name, 2)
			_localModuleMembers.handlers.onDeletePlayerHandler(_player)
			outputCore(nil,"Retour onDeletePlayerHandler du module " .. _localModuleMembers.name, 2)
		end
		_player.modules[_localModuleMembers.name] = nil
	end
	
	for _, _groupDatas in localMembers.groupsDatasOnBoard:pairs() do
		groupsOnBoard[_groupDatas:getName()] = nil
	end
	
	localMembers.groupsDatasOnBoard:removeAll()
	
	C_Player_MT:remove(_player:getIndex())
	
	outputCore(nil,"Delete Player : " .. _player:getName() , 2)
	C_Group_deleteUnit(_player:getGroup(), _player:getName())
	players[_player:getName()] = nil
	allAIUnitsAndPlayers[_player:getName()] = nil
	signalCapabilityUnits[_player:getName()] = nil
end

setmetatable(ATME.C_Player,C_Player_MT)

C_Player_MT.mt.__metatable = "Private"
C_Player_MT.mt.__index = ATME.C_Player
C_Player_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Player_MT.keys) do
		if k == key then
			found = true
		end
	end

	if found == false then
		for _, _localModuleMembers in C_Module_MT:pairs() do

			if _localModuleMembers.name == k then
				found = true
			end
		end
		
		if found == false then
			errorOutputCore(nil, "Function ATME.C_Player : Bad key " .. k)
		end
	end
	
	if found == true then
		rawset(t, k, v)
	end
end

C_Player_MT.mtModules.__metatable = "Private"
C_Player_MT.mtModules.__newindex = function(t, k, v)
	local found = false
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if _localModuleMembers.name == k then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil,"ATME : ATME.C_Player.modules Bad key " .. k)
	end
end



----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_AIUnit : gère les unités AI au format ATME. Pas de contructeur accessible
-- pour les modules externes.
-- Les instances de cette classe sont intégralement gérées par le module ATME_Core. La création et la destruction
-- dépendent du contexte de début de mission et des événements qui se produisent dans DCS en cours de mission.
-- Les modules externes accèdent à cette gestion indirectement par les handlers définis dans ceux-ci et les méthodes
-- accessibles de cette classe. Ceci permet de garantir une cohérence sur les listes internes ATME.
----------------------------------------------------------------------------------------------------------

-- C_AIUnit_MT prédéclarée avant en local
C_AIUnit_MT = {
	className = "C_AIUnit",
	mtModules = {},
	mt = {},
	
	keys = {
		"sendTransmissionOnce", "loopTransmission", "startTransmission", "stopTransmission", "stopAllTransmissions",
		"getPosition", "getVelocityVector", "getRollAxisVector", "getIndex", "getDCSUnit", "getName", "getTypeName", "getGroup", "disable",
		"getFuelRatio", "getLife", "getLifeRatio", "getCallsign", "getSpeed", "getHSpeed", "getVSpeed", "getSpeedAzimuth", "getAzimuth",
		"getCountryName", "getCoalitionName", "isHelicopter", "isPlane", "isPersonnelCarrier", "isInfantry", "isGroundVehicle", "isAirDefence", 
		"isManPad","isSAMVehicle", "isSAMSiteLauncher", "isSAMSiteRadar", "isSAMSiteCommandCenter", "isAAA", "inAir", "getMSLAltitude",  
		"getAGLAltitude", "explode", "isInDCSZone", "isInZone2D", "isInZone3D", "isNear", "crossAxisFromRightToLeft", "crossAxisFromLeftToRight", 
		"whichSide", "getNbUnitsOnBoard", "getGroupsNameOnBoard", "load", "unload", "getNearestReadyToBoardGroups",
		"fireFlare", "fireSmoke", "fireIlluminationBomb", "modules",
	},
	
	localMembers = ATME.C_IndexList(),

	add = function(self)
		local item = {
			nbUnitsOnBoard = 0,
			groupsBoarding = {},
			groupsDatasOnBoard = ATME.C_IndexList(),
			isEnabled = true,
			radioTransmissions = ATME.C_IndexList(),
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:get(_id).radioTransmissions = nil
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

ATME.C_AIUnit = {
	getClassName = function(self)
		return C_AIUnit_MT.className
	end,
	
	exists = function(_unitName)
		if type(_unitName) == "string" then
			return AIUnits[_unitName] ~= nil
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.exists : name type must be string")
		end
	end,
	
	getByName = function(_unitName)
		if type(_unitName) == "string" then
			return AIUnits[_unitName]
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.getByName : name type must be string")
		end	
	end,
}


C_AIUnit_create = function(_unit)
	local name = _unit:getName()
	local newUnit = {}
	
	setmetatable(newUnit, C_AIUnit_MT.mt)
	local index = C_AIUnit_MT:add()
	
	local unit = _unit
	local waypoints
	local typeName = _unit:getTypeName()
	local oldSide

	totalAIUnits = totalAIUnits + 1
	
	if allAIUnitsInMission[name] ~= nil then
		waypoints = allAIUnitsInMission[name].group.waypoints
	end
		
	local groupName = _unit:getGroup():getName()
	local ATMEGroup
	
	
	function newUnit:getIndex(self)
		return index
	end
	
	function newUnit.getDCSUnit(self)
		return unit
	end
	
	function newUnit.getName(self)
		return name
	end
	
	function newUnit.getTypeName(self)
		return typeName
	end
	
	function newUnit.getGroup(self)
		return ATMEGroup
	end
	
	function newUnit.disable(self)
		if groupsOnBoard[self:getGroup():getName()] == nil then
			-- Les unités d'un groupe déchargé d'un autre véhicules ne génèrent pas 
			-- d'appel à onDisableUnitHandler
				
			for _, _localModuleMembers in C_Module_MT:pairs() do
				if _localModuleMembers.handlers.onDisableAIUnitHandler ~= nil then
					-- Appel du module
					outputCore(nil,"Appel onDisableUnitHandler du module " .. _localModuleMembers.handlers.name, 2)
					_localModuleMembers.handlers.onDisableAIUnitHandler(self)
					outputCore(nil,"Retour onDisableUnitHandler du module " .. _localModuleMembers.name, 2)
				end
			end
		end
		
		local localMembers = C_AIUnit_MT:get(index)
		localMembers.isEnabled = false
		
		unit:destroy()
		C_AIUnit_delete(self)
	end
	
	function newUnit.getFuelRatio(self)
		return unit:getFuel()
	end
	
	function newUnit.getLife(self)
		return unit:getLife()
	end
	
	function newUnit.getLifeRatio(self)
		return self:getLife()/unit:getLife0()
	end
	
	function newUnit.getCallsign(self)
		local callsign = unit:getCallsign()
		
		local name = string.match(callsign, "^%a*")
		local id1 = string.match(callsign, "%d")
		local id2 = string.match(callsign, "%d$")
			
		return name, id1, id2
	end
	
	function newUnit.getPosition(self)
		return unit:getPosition().p
	end

	function newUnit.getVelocityVector(self)
		local vector = ATME.C_Vector3D(unit:getVelocity())
		return vector
	end

	function newUnit.getSpeed(self)
		local vector = self:getVelocityVector()
		return vector:getModule(vector)
	end

	function newUnit.getHSpeed(self)
		local vector = self:getVelocityVector()
		return vector:getHModule(vector)
	end

	function newUnit.getVSpeed(self)
		local vector = unit:getVelocity()
		return vector.y
	end

	function newUnit.getSpeedAzimuth(self)
		local vector = self:getVelocityVector()
			
		if vector:getModule() > 1 then 
			return vector:getAzimuth()
		else
			return 0
		end
	end

	function newUnit.getRollAxisVector(self)
		return ATME.C_Vector3D(unit:getPosition().x)
	end

	function newUnit.getAzimuth(self)
		local vector = self:getRollAxisVector()
		return vector:getAzimuth()
	end

	function newUnit.getCountryName(self)
		local id = unit:getCountry()
		return country.name[id]
	end

	function newUnit.getCoalitionName(self)
		local id = unit:getCoalition()
		return coalitionNames[id]
	end

	function newUnit.isHelicopter(self)
		return unit:hasAttribute("Helicopters")
	end
	
	function newUnit.isPersonnelCarrier(self)
		return personnelCarrierAttributes[self:getTypeName()] ~= nil
	end


	function newUnit.isPlane(self)
		return unit:hasAttribute("Planes")
	end
	
	function newUnit.isInfantry(self)
		return unit:hasAttribute("Infantry")
	end
	
	function newUnit.isGroundVehicle(self)
		return unit:hasAttribute("Ground vehicles")
	end

	function newUnit.isAirDefence(self)
		return unit:hasAttribute("Air Defence")
	end

	function newUnit.isManPad(self)
		return unit:hasAttribute("MANPADS")
	end

	function newUnit.isSAMVehicle(self, _range)
		if type(_range) == "string" then
			if _range == "SHORT" then
				return unit:hasAttribute("SR SAM")
			elseif _range == "MEDIUM" then
				return self.unit:hasAttribute("MR SAM")
			elseif _range == "LONG" then
				return self.unit:hasAttribute("LR SAM")
			elseif _range == "ALL" then
				return unit:hasAttribute("SR SAM") or self.unit:hasAttribute("MR SAM") or self.unit:hasAttribute("LR SAM")
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.isSAM : range bad value (SHORT, MEDIUM, LONG or ALL)")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.isSAM : range type must be string")
		end
	end

	function newUnit.isSAMSiteLauncher(self)
		return unit:hasAttribute("SAM LL")
	end

	function newUnit.isSAMSiteRadar(self, _category)
		if type(_category) == "string" then
			if _category == "SURFACE" then
				return unit:hasAttribute("SAM SR")
			elseif _category == "TRACKING" then
				return self.unit:hasAttribute("SAM TR")
			elseif _category == "ALL" then
				return unit:hasAttribute("SAM SR") or unit:hasAttribute("SAM TR")
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.isSAMSiteRadar : category bad value (SURFACE, TRACKING or ALL)")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.isSAMSiteRadar : category type must be string")
		end
	end

	function newUnit.isSAMSiteCommandCenter(self)
		return unit:hasAttribute("SAM CC")
	end

	function newUnit.isAAA(self)
		return unit:hasAttribute("AAA")
	end	

	function newUnit.inAir(self)
		return unit:inAir()
	end

	function newUnit.getMSLAltitude(self)
		return ATME.getPointAltitude(self:getPosition())
	end

	function newUnit.getAGLAltitude(self)
		local landHeight = ATME.getSurfaceAltitude()
		return ATME.getPointAltitude(self:getPosition()) - landHeight
	end

	function newUnit.explode(self, _power)
		if type(_power) == "number" then
			if _power >= 1 then
				ATME.explode(self:getPosition(), _power)
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.explode : power must be >= 1")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.explode : power type must be number")
		end
	end

	function newUnit.isInDCSZone(self, _name)
		if type(_name) == "string" then
			local zone = ATME.getDCSZone(_name)
			
			if zone ~= nil then
				local vector = ATME.C_Vector3D(self:getPosition(), zone.point)
				if vector:getHModule() <= zone.radius then
					return true
				else
					return false
				end
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.isInDCSZone : DCS zone " .. _name .. " unknown")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.isInDCSZone : name type must be string")
		end
	end
	
	function newUnit.isInZone2D(self, _reference, _radius)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) == true or
				ATME.isObjectClass(_reference, ATME.C_StaticObject) == true or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" then
			
			if _radius > 0 then
				local vector

				if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
							ATME.isObjectClass(_reference, ATME.C_StaticObject) then
					vector = ATME.C_Vector3D(self:getPosition(), _reference:getPosition())
				else
					vector = ATME.C_Vector3D(self:getPosition(), _reference)
				end
				
				return vector:getHModule() <= _radius
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.isInZone2D : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_AIUnit.isInZone2D : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.isInZone2D : radius type must be number")
			end
		end
	end
	
	function newUnit.isInZone3D(self, _reference, _radius)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD) and 
			type(_radius) == "number" then
			if _radius > 0 then
				local vector

				if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or ATME.isObjectClass(_reference, ATME.C_StaticObject) then
					vector = ATME.C_Vector3D(self:getPosition(), _reference:getPosition())
				else
					vector = ATME.C_Vector3D(self:getPosition(), _reference)
				end
				
				return vector:getModule() <= _radius
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.isInZone3D : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_AIUnit.isInZone3D : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.isInZone3D : radius type must be number")
			end
		end
	end
	
	function newUnit.isNear(self, _reference, _radius, _deltaAltitude)
		if (ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD) and 
				type(_radius) == "number" and type(_deltaAltitude) == "number" then
				
			if _radius > 0 then
				if _deltaAltitude > 0 then
					local vector
					if ATME.isPoint(_reference) ~= POINT_BAD then
						vector = ATME.C_Vector3D(self:getPosition(), _reference)
					else
						vector = ATME.C_Vector3D(self:getPosition(), _reference:getPosition())
					end
					
					return vector:getHModule() <= _radius and math.abs(vector:getY()) <= _deltaAltitude
				else
					errorOutputCore(nil, "Function ATME.C_AIUnit.isNear : deltaAltitude must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.isNear : radius must be > 0")
			end
		else
			if ATME.isObjectClass(_reference, ATME.C_AIUnit) == false and ATME.isObjectClass(_reference, ATME.C_Player) == false and
						ATME.isObjectClass(_reference, ATME.C_StaticObject) == false and ATME.isPoint(_reference) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_AIUnit.isNear : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.isNear : radius type must be number")
			elseif type(_deltaAltitude) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.isNear : deltaAltitude type must be number")
			end
		end
	end
	
	function newUnit.whichSide(self, _reference)
		if ATME.isObjectClass(_reference, ATME.C_AIUnit) == true or ATME.isObjectClass(_reference, ATME.C_Player) or 
				ATME.isObjectClass(_reference, ATME.C_StaticObject) or ATME.isPoint(_reference) ~= POINT_BAD then
			local rollAxis = ATME.C_Line2D(self:getRollAxisVector(), self:getPosition())
			local A = rollAxis:getOrigin()
			local B = rollAxis:getPointFromOrigin(10)
			
			local point
			if ATME.isPoint(_reference) ~= POINT_BAD then
				point = ATME.convertToPoint3D(_reference)
			else
				point = _reference:getPosition()
			end

			return ATME.whichSide(A, B, point)
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.whichSide : reference type must be ATME.C_AIUnit or ATME.C_Player or ATME.C_StaticObject or table (x,y) or (x,y,z)")
		end
	end
	
	function newUnit.crossAxisFromRightToLeft(self, _A, _B)
		if ATME.isPoint(_A) ~= POINT_BAD and ATME.isPoint(_B) ~= POINT_BAD then
			local retValue
			retValue, oldSide = crossAxis(_A, _B, self:getPosition(), oldSide, -1)
			
			return retValue
		else
			if ATME.isPoint(_A) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_AIUnit.crossAxisFromRightToLeft : A type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_B) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_AIUnit.crossAxisFromRightToLeft : B type must be table (x,y) or (x,y,z)")
			end
		end
	end
	
	function newUnit.crossAxisFromLeftToRight(self, _A, _B)
		if ATME.isPoint(_A) ~= POINT_BAD and ATME.isPoint(_B) ~= POINT_BAD then
			local retValue
			retValue, oldSide = crossAxis(_A, _B, self:getPosition(), oldSide, 1)
			
			return retValue
		else
			if ATME.isPoint(_A) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_AIUnit.crossAxisFromLeftToRight : A type must be table (x,y) or (x,y,z)")
			elseif ATME.isPoint(_B) == POINT_BAD then
				errorOutputCore(nil, "Function ATME.C_AIUnit.crossAxisFromLeftToRight : B type must be table (x,y) or (x,y,z)")
			end
		end
	end
	
	function newUnit.getNbUnitsOnBoard(self)
		local localMembers = C_AIUnit_MT:get(index)
		return localMembers.nbUnitsOnBoard
	end

	function newUnit.getGroupsNameOnBoard(self)
		local localMembers = C_AIUnit_MT:get(index)
		local temp = {}
		
		for _, _groupDatas in localMembers.groupsDatasOnBoard:pairs() do
			table.insert(temp, _groupDatas:getName())
		end
		return temp
	end

	function newUnit.unload(self, _id)
		if type(_id) == "number" then
			local err, errInfos, errDatas = C_Player_C_AIUnit_unload(self, _id)
			
			if err == true then
				if errInfos == "UNLOAD_INFANTRY_UNIT_ALREADY_EXIST" then
					errorOutputCore(nil, "Function ATME.C_AIUnit.unload : " .. errDatas .. " unit already exists !")
				elseif errInfos == "UNLOAD_GROUP_EXISTS" then
					errorOutputCore(nil, "Function ATME.C_AIUnit.unload : " .. errDatas .. " group already exists !")
				else
					return true, errInfos -- errInfos = code d'erreur
				end
			else
				return false, errInfos, errDatas  --- errInfos = spawnedGroup et errDatas = nbDeadUnits
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.unload : id type must be number")
		end
	end

	function newUnit.load(self, _groupToBoard, _radius)
		if ATME.isObjectClass(_groupToBoard, ATME.C_Group) == true and type(_radius) == "number" then
			if _groupToBoard:isOnlyInfantry() == true then
				if _radius > 0 then
					if _groupToBoard:isReadyToBoard() == true and _groupToBoard:isBoardingStarted() == false then
						if personnelCarrierAttributes[self:getTypeName()] ~= nil then
							return C_Player_C_AIUnit_load(self, _groupToBoard, _radius)
						else
							errorOutputCore(nil, "Function ATME.C_AIUnit.load : " .. self:getName() .. " not a transport unit")
						end
					else
						return true, "LOAD_INFANTRY_GROUP_NOT_READY"
					end
				else
					errorOutputCore(nil, "Function ATME.C_AIUnit.load : radius must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.load : Not infantry group")
			end
		else
			if ATME.isObjectClass(_groupToBoard, ATME.C_Group) == false then
				if _groupToBoard ~= nil then
					errorOutputCore(nil, "Function ATME.C_AIUnit.load : groupToBoard type must be ATME.C_Group")
				else
					return true, "LOAD_NO_INFANTRY_GROUP_AVAILABLE"
				end
			elseif type(_radius) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.load : radius type must be number")
			end
		end
	end

	function newUnit.getNearestReadyToBoardGroups(self, _radius)
		if type(_radius) == "number" then
			if _radius >= 0 then
				return C_Player_C_AIUnit_getNearestReadyToBoardGroups(self, _radius)
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.getNearestReadyToBoardGroups : radius must be > 0")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.getNearestReadyToBoardGroups : radius must be number")
		end
	end
	
	function newUnit.fireFlare(self, _color)
		if type(_color) == "string" then
			if flareColors[string.upper(_color)]~= nil then
				if self:getGroup():getCategoryName() == "GROUND" then
					local flare = ATME.C_Flare(_color, self:getPosition())
					flare:fireOnce()
					return false
				end
				
				return true, "AIUNIT_NOT_GROUND_UNIT"
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.fireFlare : color must be a flare color or RANDOM")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.fireFlare : color type must be string")
		end
	end

	function newUnit.fireSmoke(self, _color)
		if type(_color) == "string" then
			if smokeColors[string.upper(_color)] ~= nil then 
				if self:getGroup():getCategoryName() == "GROUND" then
					local point = self:getPosition()
					local ok = false
					local i = 0
					local newPoint = {}

					while ok == false do
						newPoint.x = point.x + math.random(-20,20)
						newPoint.y = point.z + math.random(-20,20)
						
						if ATME.isSurfaceWater(newPoint) == false then
							ok = true
						else
							 i = i + 1
							 if i == 10 then
								newPoint.x = point.x
								newPoint.y = point.y
								
								ok = true
							 end
						end
					end
					
					if ok == true then
						local smoke = ATME.C_Smoke(_color, newPoint)
						smoke:activate(false)
						
						return false
					end
					return true, "AIUNIT_CANT_FIRE_SMOKE"
				end
				
				return true, "AIUNIT_NOT_GROUND_UNIT"
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.fireSmoke : color must be a smoke color")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.fireSmoke : color type must be string")
		end
	end

	function newUnit.fireIlluminationBomb(self, _power)
		if type(_power) == "number" then
			if _power >= 1 then
				if self.getGroup():getCategoryName() == "GROUND" then
					local point = self:getPosition()
					
					point.y = point.y + 200 + math.random(0, 100)

					trigger.action.illuminationBomb(point, _power)
					
					return false
				end
				
				return true, "AIUNIT_NOT_GROUND_UNIT"
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.fireIlluminationBomb : power must be >= 1")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.fireIlluminationBomb : power type must be number")
		end
	end
	
	function newUnit.sendTransmissionOnce(self, _file, _modulation, _frequency, _power)
		if type(_file) == "string" and type(_modulation) == "string" and type(_frequency) == "number" and type(_power) == "number" then
			if _frequency > 0 then
				if _power > 0 then
					local newRadioTransmission = initRadioTransmission(_file, self:getPosition(), _modulation, _frequency, _power, self:getName())
					
					startDCSRadioTransmission(newRadioTransmission.file, newRadioTransmission.point , newRadioTransmission.modulation, 
									false, newRadioTransmission.frequency, newRadioTransmission.power, newRadioTransmission.textId)
									
					-- Transmission unique, on remove de suite pour ne pas "poluer" la gestion dans mainTimer
					activatedRadioTransmissions:remove(newRadioTransmission.entry)
				else
					errorOutputCore(nil, "Function ATME.C_AIUnit.sendTransmissionOnce : power must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.sendTransmissionOnce : frequency must be > 0")
			end
		else
			if type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.sendTransmissionOnce : file type must be string")
			elseif type(_modulation) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.sendTransmissionOnce : modulation type must be string")
			elseif type(_frequency) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.sendTransmissionOnce : frequency type must be number")
			elseif type(_power) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.sendTransmissionOnce : power type must be number")
			end
		end	
	end

	function newUnit.loopTransmission(self, _file, _modulation, _frequency, _power)
		if type(_file) == "string" and type(_modulation) == "string" and type(_frequency) == "number" and type(_power) == "number" then
			if _frequency > 0 then
				if _power > 0 then
					local localMembers = C_AIUnit_MT:get(index)
					
					local newRadioTransmission = initRadioTransmission(_file, self:getPosition(), _modulation, _frequency, _power, self:getName())
					newRadioTransmission.always = true
					
					startDCSRadioTransmission(newRadioTransmission.file, newRadioTransmission.point,
								newRadioTransmission.modulation, true, newRadioTransmission.frequency, newRadioTransmission.power, 
								newRadioTransmission.textId)
					
					local indexRadio = localMembers.radioTransmissions:add(newRadioTransmission.entry)
					return indexRadio
				else
					errorOutputCore(nil, "Function ATME.C_AIUnit.loopTransmission : power must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.loopTransmission : frequency must be > 0")
			end
		else
			if type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.loopTransmission : file type must be string")
			elseif type(_modulation) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.loopTransmission : modulation type must be string")
			elseif type(_frequency) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.loopTransmission : frequency type must be number")
			elseif type(_power) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.loopTransmission : power type must be number")
			end
		end		
	end

	function newUnit.startTransmission(self, _file, _modulation, _frequency, _power, _interval)
		if type(_file) == "string" and type(_modulation) == "string" and type(_frequency) == "number" and type(_power) == "number" and type(_interval) == "number" then
			if _frequency > 0 then
				if _power > 0 then
					if _interval >= 1 then
						local localMembers = C_AIUnit_MT:get(index)
						
						local newRadioTransmission = initRadioTransmission(_file, self:getPosition(), _modulation, _frequency, _power, self:getName())
						newRadioTransmission.interval = math.floor(_interval)
						newRadioTransmission.currentTip = 1
						newRadioTransmission.always = false
						
						startDCSRadioTransmission(newRadioTransmission.file, newRadioTransmission.point , newRadioTransmission.modulation, 
										false, newRadioTransmission.frequency, newRadioTransmission.power, newRadioTransmission.textId)
										
						local indexRadio = localMembers.radioTransmissions:add(newRadioTransmission.entry)
						return indexRadio
					else
						errorOutputCore(nil, "Function ATME.C_AIUnit.startTransmission : interval must be >= 1")
					end
				else
					errorOutputCore(nil, "Function ATME.C_AIUnit.startTransmission : power must be > 0")
				end
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.startTransmission : frequency must be > 0")
			end
		else
			if type(_file) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.startTransmission : file type must be string")
			elseif type(_modulation) ~= "string" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.startTransmission : modulation type must be string")
			elseif type(_frequency) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.startTransmission : frequency type must be number")
			elseif type(_power) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.startTransmission : power type must be number")
			elseif type(_interval) ~= "number" then
				errorOutputCore(nil, "Function ATME.C_AIUnit.startTransmission : interval type must be number")
			end
		end		
	end

	function newUnit.stopTransmission(self, _entry)
		if type(_entry) == "number" then
			if _entry >= 1 then
				local entry = math.floor(_entry)
				local localMembers = C_AIUnit_MT:get(index)
				if localMembers.radioTransmissions:get(entry) ~=nil then
					local radioTransmission = activatedRadioTransmissions:get(localMembers.radioTransmissions:get(entry))
					if radioTransmission.always == true then
						stopDCSRadioTransmission(radioTransmission.textId)
					end
						
					activatedRadioTransmissions:remove(localMembers.radioTransmissions:get(entry))
					localMembers.radioTransmissions:remove(entry)
				end
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.stopTransmission : entry must be >= 1")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.stopTransmission : entry type must be number")
		end	
	end

	function newUnit.stopAllTransmissions(self)
		local localMembers = C_AIUnit_MT:get(index)
		
		for _id, _ in localMembers.radioTransmissions:pairs() do
			self:stopTransmission(_id)
		end
	end
	
	if ATME.C_Group.exists(groupName) == false then
		C_Group_create(newUnit)
	else
		C_Group_addUnit(groups[groupName], newUnit)
	end
	
	ATMEGroup = groups[groupName]
	AIUnits[name] = newUnit
	allAIUnitsAndPlayers[name] = newUnit
	
	if personnelCarrierAttributes[newUnit:getTypeName()] ~= nil and newUnit:isHelicopter() == true then
		signalCapabilityUnits[name] = newUnit
	end
	
	newUnit.modules = {}
	setmetatable(newUnit.modules, C_AIUnit_MT.mtModules)
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if newUnit.modules[_localModuleMembers.name] == nil then
			newUnit.modules[_localModuleMembers.name] = {}
		end
		if groupsOnBoard[groupName] == nil then
			-- Les unités d'un groupe déchargé d'un autre véhicule ne génèrent pas 
			-- d'appel à onCreateAIUnitHandler
				
			if _localModuleMembers.handlers.onCreateAIUnitHandler ~= nil then
				-- Appel du module
				outputCore(nil,"Appel onCreateAIUnitHandler du module " .. _localModuleMembers.name, 2)
				_localModuleMembers.handlers.onCreateAIUnitHandler(newUnit)
				outputCore(nil,"Retour onCreateAIUnitHandler du module " .. _localModuleMembers.name, 2)
			end
		end
	end
	
	outputCore(nil,"Create unit = " .. name, 2)
end

C_AIUnit_delete = function(_AIUnit)
	local localMembers = C_AIUnit_MT:get(_AIUnit:getIndex())
	_AIUnit:stopAllTransmissions()
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if groupsOnBoard[_AIUnit:getGroup():getName()] == nil then
			-- Les unités d'un groupe déchargé d'un autre véhicule ne génèrent pas 
			-- d'appel à onDeleteAIUnitHandler
			
			if _localModuleMembers.handlers.onDeleteAIUnitHandler ~= nil then
				-- Appel du module
				outputCore(nil,"Appel onDeleteAIUnitHandler du module " .. _localModuleMembers.name, 2)
				_localModuleMembers.handlers.onDeleteAIUnitHandler(_AIUnit, localMembers.isEnabled)
				outputCore(nil,"Retour onDeleteAIUnitHandler du module " .. _localModuleMembers.name, 2)
			end
		end
		_AIUnit.modules[_localModuleMembers.name] = nil
	end
	
	for _, _groupDatas in localMembers.groupsDatasOnBoard:pairs() do
		groupsOnBoard[_groupDatas:getName()] = nil
	end
	
	localMembers.groupsDatasOnBoard:removeAll()
	
	C_AIUnit_MT:remove(_AIUnit:getIndex())
	
	outputCore(nil,"Delete unit = " .. _AIUnit:getName(), 2)
	
	C_Group_deleteUnit(_AIUnit:getGroup(), _AIUnit:getName())
	AIUnits[_AIUnit:getName()] = nil
	allAIUnitsAndPlayers[_AIUnit:getName()] = nil
	signalCapabilityUnits[_AIUnit:getName()] = nil
end

setmetatable(ATME.C_AIUnit,C_AIUnit_MT)


C_AIUnit_MT.mt.__metatable = "Private"
C_AIUnit_MT.mt.__index = ATME.C_AIUnit
C_AIUnit_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_AIUnit_MT.keys) do
		if k == key then
			found = true
		end
	end

	if found == false then
		for _, _localModuleMembers in C_Module_MT:pairs() do

			if _localModuleMembers.name == k then
				found = true
			end
		end
		
		if found == false then
			errorOutputCore(nil, "Function ATME.C_AIUnit : Bad key " .. k)
		end
	end
	
	if found == true then
		rawset(t, k, v)
	end
end

C_AIUnit_MT.mtModules.__metatable = "Private"
C_AIUnit_MT.mtModules.__newindex = function(t, k, v)
	local found = false
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if _localModuleMembers.name == k then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil,"ATME : ATME.C_AIUnit.modules Bad key " .. k)
	end
end


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_StaticObject : gère les objets statiques au format ATME. Pas de contructeur accessible
-- pour les modules externes. 
-- Sont exclus les objets statiques FARP et Airfieds qui sont gérés par ATME.C_Airbase.
-- Les instances de cette classe sont intégralement gérées par le module ATME_Core. La création et la destruction
-- dépendent du contexte de début de mission et des événements qui se produisent dans DCS en cours de mission.
-- Les modules externes accèdent à cette gestion indirectement par les handlers définis dans ceux-ci et les méthodes
-- accessibles de cette classe. Ceci permet de garantir une cohérence sur les listes internes ATME.
----------------------------------------------------------------------------------------------------------

local C_StaticObject_MT = {
	className = "C_StaticObject",
	mtModules = {},
	mt = {},
	
	keys = {
		"getCategory", "getLife", "getDCSStaticObject", "getName", "getTypeName", "getPosition", "getAzimuth",
		"getCountryName", "getCoalitionName", "explode", "fireFlare", "fireSmoke", "fireIlluminationBomb", "modules",
	},
}

ATME.C_StaticObject = {
	getClassName = function(self)
		return C_StaticObject_MT.className
	end,
	
	exists = function(_objectName)
		if type(_objectName) == "string" then
			return staticObjects[_objectName] ~= nil
		else
			errorOutputCore(nil, "Function ATME.C_StaticObject.exists : name type must be string")
		end
	end,
	
	getByName = function(_objectName)
		if type(_objectName) == "string" then
			return staticObjects[_objectName]
		else
			errorOutputCore(nil, "Function ATME.C_StaticObject.getByName : name mst be string")
		end	
	end,
}

local function C_StaticObject_create(_object)
	local name = _object:getName()

	local newObject = {}
	
	setmetatable(newObject, C_StaticObject_MT.mt)
	
	local object = _object
	local typeName = _object:getTypeName()
	local category

	if allStaticObjectsInMission[name] ~= nil then
		category = allStaticObjectsInMission[name].category
	else
		category = newStaticObjectsCategory[name]
		newStaticObjectsCategory[name] = nil
	end
	
	
	
	function newObject.getDCSStaticObject(self)
		return object
	end
	
	function newObject.getName(self)
		return name
	end
	
	function newObject.getCategory()
		-- Catégories objets statiques
		-- 		Ships
		-- 		Planes
		-- 		Helicopters
		-- 		Warehouses
		-- 		Cargos
		-- 		Unarmed (tous ce qui est véhicules terrestre)
		-- 		Fortifications
		
		return category
	end
	
	function newObject.getTypeName(self)
		return typeName
	end
	
	function newObject.getLife(self)
		return object:getLife()
	end
	
	function newObject.getPosition(self)
		return object:getPosition().p
	end

	function newObject.getAzimuth(self)
		local vector = ATME.C_Vector3D(object:getPosition().x)
		return vector:getAzimuth()
	end

	function newObject.getCountryName(self)
		local id = object:getCountry()
		return country.name[id]
	end

	function newObject.getCoalitionName(self)
		local id = object:getCoalition()
		return coalitionNames[id]
	end

	function newObject.explode(self, _power)
		if type(_power) == "number" then
			if _power >= 1 then
				ATME.explode(self:getPosition(), _power)
			else
				errorOutputCore(nil, "Function ATME.C_StaticObject.explode : power must be >= 1")
			end
		else
			errorOutputCore(nil, "Function ATME.C_StaticObject.explode : power type must be number")
		end
	end
	
	function newObject.fireFlare(self, _color)
		if type(_color) == "string" then
			if flareColors[string.upper(_color)]~= nil then
				local flare = ATME.C_Flare(_color, self:getPosition())
				flare:fireOnce()
			else
				errorOutputCore(nil, "Function ATME.C_StaticObject.fireFlare : color must be a flare color or RANDOM")
			end
		else
			errorOutputCore(nil, "Function ATME.C_StaticObject.fireFlare : color type must be string")
		end
	end

	function newObject.fireSmoke(self, _color)
		if type(_color) == "string" then
			if smokeColors[string.upper(_color)]~= nil then 
				local point = self:getPosition()
				local ok = false
				local i = 0
				local newPoint = {}

				while ok == false do
					newPoint.x = point.x + math.random(-20,20)
					newPoint.y = point.z + math.random(-20,20)
					
					if ATME.isSurfaceWater(newPoint) == false then
						ok = true
					else
						 i = i + 1
						 if i == 10 then
							newPoint.x = point.x
							newPoint.y = point.y
							
							ok = true
						 end
					end
				end
				
				if ok == true then
					local smoke = ATME.C_Smoke(_color, newPoint)
					smoke:activate(false)
					
					return false
				end
				
				return true, "AIUNIT_CANT_FIRE_SMOKE"
			else
				errorOutputCore(nil, "Function ATME.C_StaticObject.fireSmoke : color must be a smoke color")
			end
		else
			errorOutputCore(nil, "Function ATME.C_StaticObject.fireSmoke : color type must be string")
		end
	end

	function newObject.fireIlluminationBomb(self, _power)
		if type(_power) == "number" then
			if _power >= 1 then
				local point = self:getPosition()
				
				point.y = point.y + 200 + math.random(0, 100)

				trigger.action.illuminationBomb(point, _power)
			else
				errorOutputCore(nil, "Function ATME.C_AIUnit.fireIlluminationBomb : power must be >= 1")
			end
		else
			errorOutputCore(nil, "Function ATME.C_AIUnit.fireIlluminationBomb : power type must be number")
		end
	end

	staticObjects[name] = newObject
	
	newObject.modules = {}
	setmetatable(newObject.modules, C_StaticObject_MT.mtModules)
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if newObject.modules[_localModuleMembers.name] == nil then
			newObject.modules[_localModuleMembers.name] = {}
		end
		if _localModuleMembers.handlers.onCreateStaticObjectHandler ~= nil then
			-- Appel du module
			outputCore(nil,"Appel onCreateStaticObjectHandler du module " .. _localModuleMembers.name, 2)
			_localModuleMembers.handlers.onCreateStaticObjectHandler(newObject)
			outputCore(nil,"Retour onCreateStaticObjectHandler du module " .. _localModuleMembers.name, 2)
		end
	end
	
	outputCore(nil,"Create static unit " .. name, 2)
end

C_StaticObject_delete = function(_staticUnit)
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if _localModuleMembers.handlers.onDeleteStaticObjectHandler ~= nil then
			-- Appel du module
			outputCore(nil,"Appel onDeleteStaticObjectHandler du module " .. _localModuleMembers.name, 2)
			_localModuleMembers.handlers.onDeleteStaticObjectHandler(_staticUnit)
			outputCore(nil,"Retour onDeleteStaticObjectHandler du module " .. _localModuleMembers.name, 2)
		end
		_staticUnit.modules[_localModuleMembers.name] = nil
	end
	
	staticObjects[_staticUnit:getName()] = nil
	outputCore(nil,"Delete unit = " .. _staticUnit:getName(), 2)
end

setmetatable(ATME.C_StaticObject,C_StaticObject_MT)

C_StaticObject_MT.mt.__metatable = "Private"
C_StaticObject_MT.mt.__index = ATME.C_StaticObject
C_StaticObject_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_StaticObject_MT.keys) do
		if k == key then
			found = true
		end
	end

	if found == false then
		for _, _localModuleMembers in C_Module_MT:pairs() do

			if _localModuleMembers.name == k then
				found = true
			end
		end
		
		if found == false then
			errorOutputCore(nil, "Function ATME.C_StaticObject : Bad key " .. k)
		end
	end
	
	if found == true then
		rawset(t, k, v)
	end
end

C_StaticObject_MT.mtModules.__metatable = "Private"
C_StaticObject_MT.mtModules.__newindex = function(t, k, v)
	local found = false
	
	for _, _localModuleMembers in C_Module_MT:pairs() do
		if _localModuleMembers.name == k then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil,"ATME : ATME.C_StaticObject.modules Bad key " .. k)
	end
end


----------------------------------------------------------------------------------------------------------
-- Classe ATME.C_Race : gère les courses de joueurs. Contructeur accessible pour les modules externes. 
-- Une course de joueurs est composées de portes définie par un côté gauche et un côté droit (sens de passage
-- de la porte par le joueur). Les temps sont gérés au 1/100è. une course est associée au module l'ayant créée
-- et donc les events ATME générés seront transmis uniquement ce module. Le description des events est données
-- dans la fonction _timerx100.
-- Le nombre maximal de portes est à 100.
----------------------------------------------------------------------------------------------------------

local C_Race_MT = {
	className = "C_Race",
	mt = {},
	
	keys = {
		"getNbTurns", "getNbDoors", "getIndex", "getName", "delete", "getDoorSides", "getLapAtDoor", "setLapAtDoor",
		"getDoorIndex", "displayForAllPlayers", "soundForAllPlayers", "addPlayer", "removePlayer", "removeAllPlayers",
		"isPlayerInRace", "getRanking", "setMissedDoorRule", "getMissedDoorPenalty", "setMaxAltitudeRule", "getMaxAltitudePenalty",
		"getMaxAltitude", "resetMissedDoorRule", "resetMaxAltitudeRule", "getPlayerNextDoorIndex",
	},
	
	localMembers = ATME.C_IndexList(),
	
	add = function(self, _doorsIndex)
		local item = {
			missedDoorPenalty = 0,
			maxAltitude = -1,
			maxAltitudePenalty = 0,
			players = {},
			doorsIndex = _doorsIndex,
			bestPlayers = {},
			bestTurn = 
			{
				pseudo = "",
				bestTime = -1,
			},
		}
		
		return self.localMembers:add(item)
	end,
	
	remove = function(self, _id)
		self.localMembers:remove(_id)
	end,
	
	get = function(self, _id)
		return self.localMembers:get(_id)
	end,
}

ATME.C_Race = {
	getClassName = function(self)
		return C_Race_MT.className
	end,
	
	exists = function(_raceName)
		if type(_raceName) == "string" then
			return races[_raceName] ~= nil
		else
			errorOutputCore(nil, "Function ATME.C_Race.exists : name type must be string")
		end
	end,
	
	getByName = function(_raceName)
		if type(_raceName) == "string" then
			return races[_raceName]
		else
			errorOutputCore(nil, "Function ATME.C_Race.getByName : name type must be string")
		end	
	end,
}

local function C_Race_createEvent(_race, _type, _datas)
	local localMembers = C_Race_MT:get(_race:getIndex())
	
	_datas.race = _race

	local item = {
		type = _type,
		datas = _datas,
	}
	
	C_EventsMgr_addCoreEvent(item)
end

local function C_Race_create(_name, _leftSideDoorName, _rightSideDoorName, _nbTurns)
	local doors = ATME.C_IndexList()
	local doorsIndex = {}
	
	for _i = 1, 100 do -- Identifiant de porte dans l'éditeur de mission : de #001 à #100
		local index = string.format("%03d", _i)
		if ATME.C_StaticObject.exists(_leftSideDoorName .. " #" .. index) == true then
			if ATME.C_StaticObject.exists(_rightSideDoorName .. " #" .. index) == true then
				local leftObject = ATME.C_StaticObject.getByName(_leftSideDoorName .. " #" .. index)
				local rightObject = ATME.C_StaticObject.getByName(_rightSideDoorName .. " #" .. index)
				
				local vector = ATME.C_Vector3D(leftObject:getPosition(), rightObject:getPosition())
				
				if vector:getHModule() < 500 then -- Largeur de porte maximale : 500m
					local door = {
						index = _i,
						leftObject = leftObject,
						rightObject = rightObject,
						lap = false,
						bestTurn = {
							pseudo = "",
							bestTime = -1,
						},
					}
					door.id = doors:add(door)
					doorsIndex[_i] = door
				else
					errorOutputCore(nil, "Function ATME.C_Race : Race " .. _name .. " - Door #" .. index .. " too large (>500m)")
				end
			else
				errorOutputCore(nil, "Function ATME.C_Race : Race " .. _name .. " - Door #" .. index .. " no right side")
			end
		elseif ATME.C_StaticObject.exists(_rightSideDoorName .. " #" .. index) == true then
			errorOutputCore(nil, "Function ATME.C_Race : Race " .. _name .. " - Door #" .. index .. " no left side")
		end
	end
	
	if exitATME == false then
		if doors:getCount() >= 2 then
			local newRace = {}
			local nbTurns = math.floor(_nbTurns)
			local name = _name
		
			local index = C_Race_MT:add(doorsIndex)
			
			setmetatable(newRace, C_Race_MT.mt)
		
			function newRace.getIndex(self)
				return index
			end
			
			function newRace.getName(self)
				return name
			end
		
			function newRace.getNbTurns(self)
				return nbTurns
			end
			
			function newRace.getNbDoors(self)
				return doors:getCount()
			end
			
			function newRace.getDoorSides(self, _index)
				if type(_index) == "number" then
					local localMembers = C_Race_MT:get(index)
					local index = math.floor(_index)
					if index > 0 and localMembers.doorsIndex[index] ~= nil then 
						local leftObject = localMembers.doorsIndex[index].leftObject
						local rightObject = localMembers.doorsIndex[index].rightObject
						
						return leftObject, rightObject
					else
						errorOutputCore(nil, "Function ATME.C_Race.getDoorSides : Race " .. name .. " - Door #" .. index .. " not found")
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.getDoorSides : offset type must be number")
				end
			end
			
			function newRace.getLapAtDoor(self, _index)
				if type(_index) == "number" then
					local localMembers = C_Race_MT:get(index)
					local index = math.floor(_index)
					if index > 0 and localMembers.doorsIndex[index] ~= nil then 
						return localMembers.doorsIndex[index].lap
					else
						errorOutputCore(nil, "Function ATME.C_Race.getLapAtDoor : Door #" .. index .. " not found")
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.getLapAtDoor : index type must be number")
				end
			end
			
			function newRace.setLapAtDoor(self, _index)
				if type(_index) == "number" then
					local localMembers = C_Race_MT:get(index)
					
					local index = math.floor(_index)
					if index > 0 and localMembers.doorsIndex[index] ~= nil then 
						if localMembers.doorsIndex[index].id > 1 and  localMembers.doorsIndex[index].id < self:getNbDoors() then
							localMembers.doorsIndex[index].lap = true
						else
							errorOutputCore(nil, "Function ATME.C_Race.setLapAtDoor : Race " .. name .. " - No lap on first or last door")
						end
					else
						errorOutputCore(nil, "Function ATME.C_Race.setLapAtDoor : Race " .. name .. " - Door #" .. index .. " not found")
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.setLapAtDoor : index type must be number")
				end
			end
			
			function newRace.getDoorIndex(self, _offset)
				if type(_offset) == "number" then
					local offset = math.floor(_offset)
					if offset > 0 and _offset <= doors:getCount() then
						return doors:get(offset).index
					else
						errorOutputCore(nil, "Function ATME.C_Race.getDoorIndex : Race " .. name .. " - " .. _offset .. " must be > 0 and <= " .. doors:getCount())
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.setLapAtDoor : offset type must be number")
				end
			end
			
			function newRace.getPlayerNextDoorIndex(self, _player)
				if ATME.isObjectClass(_player, ATME.C_Player) == true then
					local localMembers = C_Race_MT:get(index)
					if localMembers.players[_player:getName()] ~= nil then
						return self:getDoorIndex(localMembers.players[_player:getName()].nextDoor)
					else
						errorOutputCore(nil, "Function ATME.C_Race.getPlayerNextDoorIndex : player " .. _player:getPseudo() .. " not in race " .. raceName)
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.getPlayerNextDoorIndex : player type must be ATME.C_Player")
				end
			end
			
			function newRace.displayForAllPlayers(self, _text, _duration)
				if type(_text) == "string" and type(_duration) == "number" then
					local localMembers = C_Race_MT:get(index)
					
					for _name, _player in pairs(localMembers.players) do
						_player.player:display(_text, _duration)
					end
				else
					if type(_text) ~= "string" then
						errorOutputCore(nil, "Function ATME.C_Race.displayForAllPlayers : text type must be string")
					elseif type(_duration) ~= "number" then
						errorOutputCore(nil, "Function ATME.C_Race.displayForAllPlayers : duration type must be number")
					end
				end
			end
			
			function newRace.soundForAllPlayers(self, _file)
				if type(_file) == "string" then
					local localMembers = C_Race_MT:get(index)
					
					for _, _player in pairs(localMembers.players) do
						_player.player:soundOnce(_file)
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.soundForAllPlayers : file type must be string")
				end
			end
			
			function newRace.addPlayer(self, _player)
				if ATME.isObjectClass(_player, ATME.C_Player) == true then
					local found = false
					local raceName = ""
					for _, _race in pairs(races) do
						local localMembers = C_Race_MT:get(_race:getIndex())
						
						if localMembers.players[_player:getName()] ~= nil then
							found = true
							raceName = _race:getName()
						end
					end
					
					if found == false then
						local localMembers = C_Race_MT:get(index)
						
						newPlayer = {
							player = _player,
							bestTime = -1,
							recordTime = 0,
						}
						
						table.insert(localMembers.bestPlayers, newPlayer)
						
						localMembers.players[_player:getName()] = {
							player = _player,
							startTime = 0,
							startTurnTime = 0,
							bestTurnTime = -1,
							bestTime = -1,
							penalty = 0,
							nextDoor = 1,
							currentTurn = 1,
							oldSide = nil,
						}
					else
						errorOutputCore(nil, "Function ATME.C_Race.addPlayer : player " .. _player:getPseudo() .. " already in race " .. raceName)
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.addPlayer : player type must be ATME.C_Player")
				end
			end
	
			function newRace.removePlayer(self, _player)
				if ATME.isObjectClass(_player, ATME.C_Player) == true then
					local localMembers = C_Race_MT:get(index)
					
					if localMembers.players[_player:getName()] ~= nil then
						local index = 1
						while index <= #localMembers.bestPlayers do
							if localMembers.bestPlayers[index].player == _player then
								table.remove(localMembers.bestPlayers, index)
							else
								index = index + 1
							end
						end
						
						localMembers.players[_player:getName()] = nil
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.removePlayer : player type must be ATME.C_Player")
				end
			end
	
			function newRace.removeAllPlayers(self)
				local localMembers = C_Race_MT:get(index)
				for _name, _ in pairs(localMembers.players) do
					localMembers.players[_name] = nil
				end
				
				while #localMembers.bestPlayers > 0 do
					table.remove(localMembers.bestPlayers, #localMembers.bestPlayers)
				end
			end
	
			function newRace.isPlayerInRace(self, _player)
				if ATME.isObjectClass(_player, ATME.C_Player) == true then
					local localMembers = C_Race_MT:get(index)
					
					return localMembers.players[_player:getName()] ~= nil
				else
					errorOutputCore(nil, "Function ATME.C_Race.isPlayerInRace : player type must be ATME.C_Player")
				end
			end
	
			function newRace.getRanking(self)
				local localMembers = C_Race_MT:get(index)
				local ranking = {}
				
				for _, _item in ipairs(localMembers.bestPlayers) do
					local item = {
						player = _item.player,
						bestTime = _item.bestTime,
					}
					
					table.insert(ranking, item)
				end
				
				return ranking
			end
	
			function newRace.setMissedDoorRule(self, _penalty)
				if type(_penalty) == "number" then
					if _penalty >= 0 then
						local localMembers = C_Race_MT:get(index)
						localMembers.missedDoorPenalty = math.floor(_penalty)
					else
						errorOutputCore(nil, "Function ATME.C_Race.setMissedDoorRule : penalty must be >= 0")
					end
				else
					errorOutputCore(nil, "Function ATME.C_Race.setMissedDoorRule : penalty type must be number")
				end
			end
			
			function newRace.resetMissedDoorRule(self)
				local localMembers = C_Race_MT:get(index)
				localMembers.missedDoorPenalty = 0
			end
	
			function newRace.getMissedDoorPenalty(self)
				local localMembers = C_Race_MT:get(index)
				return localMembers.missedDoorPenalty
			end
	
			function newRace.setMaxAltitudeRule(self, _maxAltitude, _penalty)
				if type(_maxAltitude) == "number" and type(_penalty) == "number" then
					local localMembers = C_Race_MT:get(index)
					if _maxAltitude > 0 then
						localMembers.maxAltitude = _maxAltitude
						
						if _penalty >= 0 then
							localMembers.maxAltitudePenalty = _penalty
						else
							errorOutputCore(nil, "Function ATME.C_Race.setMaxAltitudeRule : penalty must be >= 0")
						end
					else
						errorOutputCore(nil, "Function ATME.C_Race.setMaxAltitudeRule : Bad altitude max")
					end
				else
					if type(_maxAltitude) ~= "number" then
						errorOutputCore(nil, "Function ATME.C_Race.setMaxAltitudeRule : maxAltitude type must be number")
					elseif type(_penalty) ~= "number" then
						errorOutputCore(nil, "Function ATME.C_Race.setMaxAltitudeRule : penalty type must be number")
					end
				end
			end
			
			function newRace.resetMaxAltitudeRule(self)
				local localMembers = C_Race_MT:get(index)
				localMembers.maxAltitude = -1
				localMembers.maxAltitudePenalty = 0
			end
	
			function newRace.getMaxAltitudePenalty(self)
				local localMembers = C_Race_MT:get(index)
				return localMembers.maxAltitudePenalty
			end
	
			function newRace.getMaxAltitude(self)
				local localMembers = C_Race_MT:get(index)
				return localMembers.maxAltitude
			end
			
			function newRace.delete(self)
				self:removeAllPlayers()
				doors = nil
				races[name] = nil
			end
			
			races[name] = newRace
			return newRace
		else
			errorOutputCore(nil, "Function ATME.C_Race : Race " .. _name .. " must have two doors at least")
		end
	end
end

setmetatable(ATME.C_Race,C_Race_MT)

C_Race_MT.__call = function(t, _name, _leftSideDoorName, _rightSideDoorName, _nbTurns)
	if type(_name) == "string" and type(_leftSideDoorName) == "string" and type(_rightSideDoorName) == "string" and type(_nbTurns) == "number" then
		if races[_name] == nil then
			if _nbTurns >= 1 then
				return C_Race_create(_name, _leftSideDoorName, _rightSideDoorName, math.floor(_nbTurns))
			else
				errorOutputCore(nil, "Function ATME.C_Race : Race " .. _name .. " - nbTurns must be >= 1")
			end
		else
			errorOutputCore(nil, "Function ATME.C_Race : Race " .. _name .. " already exists")
		end
	else
		if type(_name) ~= "string" then
			errorOutputCore(nil, "Function ATME.C_Race : name type must be string")
		elseif type(_leftSideDoorName) ~= "string" then
			errorOutputCore(nil, "Function ATME.C_Race : leftSideDoorName type must be string")
		elseif type(_rightSideDoorName) ~= "string" then
			errorOutputCore(nil, "Function ATME.C_Race : rightSideDoorName type must be string")
		elseif type(_nbTurns) ~= "number" then
			errorOutputCore(nil, "Function ATME.C_Race : nbTurns type must be number")
		end
	end
end

C_Race_MT.mt.__metatable = "Private"
C_Race_MT.mt.__index = ATME.C_Race
C_Race_MT.mt.__newindex = function(t, k, v)
	local found = false

	for _, key in ipairs(C_Race_MT.keys) do
		if k == key then
			found = true
		end
	end
	
	if found == true then
		rawset(t, k, v)
	else
		errorOutputCore(nil, "Function ATME.C_Race : Bad key " .. k)
	end
end

----------------------------------------------------------------------------------------------------------
-- FIN DE DEFINITION DES CLASSES D'OBJETS DEFINIS DANS ATME
--
-- FONCTIONS DE GESTION ATME_CORE
----------------------------------------------------------------------------------------------------------

do
	----------------------------------------------------------------------------------------------------------
	-- Fonction de mise à jour (appelé par les 2 timers _timerMainFunction et _timer100x) des joueurs actifs :
	--		-> Tous les joueurs actifs dans la liste ATME au coup précédent sont listés
	--		-> On ajuste avec les joueurs toujours actifs (fonction DCS coalition.getPlayers)
	--		-> Les joeurs que ne sont pas actifs sont détruits de la liste ATME.
	-- Cette fonction est importante car ATME ne se base que sur l'événement DCS S_EVENT_BIRTH. En cas de changement
	-- de slot par exemple, cet événement est émis. Mais pas la destruction. 
	-- Les événements S_EVENT_PLAYER_ENTER ou S_EVENT_PLAYER_LEAVE ont buggé par le passé en multi,
	-- donc pas de gestion de ces événements pour la mise à jour interne ATME.
	--
	-- Elle permet lors dudit timer d'éviter les incohérences qui pourraient se produire dans la liste ATME joueurs.
	----------------------------------------------------------------------------------------------------------
	local function updateCurrentPlayers()
		-- On repositionne tous les joueurs comme n'étant plus actifs.
		
		local lastActivePlayer = {}
		for _, _player in pairs(players) do
			lastActivePlayer[_player:getName()] = _player:getName()
		end

		for i = 0, 2 do
			local newPlayerUnits = coalition.getPlayers(i)
			for _, _newPlayer in ipairs(newPlayerUnits) do
				if ATME.C_Player.exists(_newPlayer:getName()) == true then
					lastActivePlayer[_newPlayer:getName()] = nil
				end
			end
		end
		

		-- On met à jour les joueurs qui ne sont plus actifs
		-- donc disparus depuis le dernier passage
		
		for _, playerName in pairs(lastActivePlayer) do
			C_Player_delete(players[playerName])
		end
	end
	
	----------------------------------------------------------------------------------------------------------
	-- Fonction _timerMainFunction est appelée à l'initialisation voir plus bas, puis réarmée toute les secondes.
	-- Cette fonction permet :
	--		- la mise à jour permanente des listes ATME des joueurs et unités AI.
	-- 		- la gestion des smokes (classe ATME.C_Smoke) qui doivent rester actifs et les tirs répétés de 
	-- 		   flares (classe ATME.C_Flare)
	-- 		- la gestion les radios transmissions permanentes affectées aux unités AI (classe ATME.AIUnit)
	--		- la gestion des sons répétés associés aux joueurs (classe ATME.C_Player)
	-- 		- le complément la liste des events actifs en ajoutant les events dus aux UserTrigger (classe ATME.C_UserTrigger)
	-- 		  Il s'agit d'un complément car le _timer100x ajoute en permanence les events ATME de la classe ATME.C_RACE générés
	-- 		  depuis le dernier appel au timer _timerMainFunction
	--		- l'appel pour chaque module du handler onTimerHandler (appel unique/module) avec en paramètre la liste des 
	--		  events actifs
	-- 		- l'appel des fonctions de mise des joueurs et unités AI
	--		- après ces appels, la liste des events actifs est remise à 0.
	----------------------------------------------------------------------------------------------------------
	local _timerMainFunction
    _timerMainFunction = function()
		if exitATME == false then
			-- Fonction des mises à jour de la liste des joueurs ... 
			-- Important pour éviter les incohérences, les timers étant asynchrones
			updateCurrentPlayers()
			
			local currentTime = ATME.getTime()
			local currentTipsCount = math.floor(currentTime)
		
			-- Contrôle des radiosTransmissions non ponctuelles
			for _, _radioTransmission in activatedRadioTransmissions:pairs() do
				if _radioTransmission.always == false then -- Cas du loop est particulier (always à true dans ce cas) car géré par DCS.
					if _radioTransmission.currentTip == _radioTransmission.interval then
						startDCSRadioTransmission(_radioTransmission.file, _radioTransmission.point , _radioTransmission.modulation, false, 
									_radioTransmission.frequency, _radioTransmission.power, _radioTransmission.textId)
						_radioTransmission.currentTip = 1
					else
						_radioTransmission.currentTip = _radioTransmission.currentTip + 1
					end
				end
			end
			
			-- Contrôle des smokes qui se réactivent
			for _, _smoke in activatedSmokes:pairs() do
				local localMembers = C_Smoke_MT:get(_smoke:getIndex())
				
				if localMembers.timeStart + maxTimeSmoke <= currentTime then
					local color = smokeColors[_smoke:getColor()]
					trigger.action.smoke(_smoke:getPoint(), color)
					localMembers.timeStart = currentTime
				end
			end
			
			-- Contrôle des flares qui se réactivent
			for _, _flare in activatedFlares:pairs() do
				if _flare:getTimeStart() + _flare:getDuration() > currentTime then
					if  math.random(1,2) == 1 then
						_flare:fireOnce()
					end
				else
					_flare:stop()
				end
			end
			
			-- Contrôle des sons joueur qui se réactivent
			for _, _player in activatedSounds:pairs() do
				local localMembers = C_Player_MT:get(_player:getIndex())
				
				if localMembers.pauseStartTime + localMembers.pauseDuration > currentTime then
					localMembers.pauseDuration = 0
					localMembers.pauseStartTime = 0
				end
				
				if localMembers.soundStartTime + localMembers.soundInterval <= currentTime and localMembers.pauseDuration == 0 then
					if localMembers.randomRead == true then 
						localMembers.currentReadIndex = math.random(1, #localMembers.playList)
					else
						localMembers.currentReadIndex = localMembers.currentReadIndex + 1
						if localMembers.currentReadIndex > #localMembers.playList then
							localMembers.currentReadIndex = 1
						end
					end
					
					if localMembers.soundIntervalNext > 0 then
						localMembers.soundInterval = localMembers.soundIntervalNext
						localMembers.soundIntervalNext = 0
					end
					trigger.action.outSoundForGroup(_player:getGroupId(), localMembers.playList[localMembers.currentReadIndex])
					localMembers.soundStartTime = currentTime
				end
			end
			
			-- Contrôle des pickupZones
			for _name, _pickupZone in pairs(pickupZones) do
				for _, _group in _pickupZone.groups:pairs() do
					local group = ATME.C_Group.getByName(_group.groupName)
					if group ~= nil then
						if _group.isOk == true then
							-- Lors d'un spawn la route n'est intialisée de suite du group:stop (fait dans changeReadyToBoard)
							-- est inefficace... 
							-- Au premier coup, isOk passe à true, et l'action sera menée au coup suivant.
							
							local zone = ATME.getDCSZone(_name)
							
							local distance = group:getMinHDistance(zone.point)
					
							if distance <= zone.radius then
								_group.isInZone = true
								if math.random() < _group.randomMax or group:getFirstUnit():getSpeed() == 0 then
									group:changeReadyToBoard(true)
								end
							else
								-- Si le groupe ressort de la zone et que le groupe n'est pas passé à changeReadyToBoard -> True alors on force
								if _group.isInZone == true then
									group:changeReadyToBoard(true)
								end
							end
						elseif group:isReadyToBoard() == false then
							_group.isOk = true
						end
					end
				end
			end
			
			-- Contrôle des signaux
			-- DEBUG outputCore(nil, "Nb signaux actifs : " .. signals:getCount(),1)
			for _index, _signal in signals:pairs() do
				local signalGroup = ATME.C_Group.getByName(_signal.groupName)

				if signalGroup ~= nil then
					local firstUnit = signalGroup:getFirstUnit()
					if _signal.timeStart < 0 then
						local unitNamesInZone = {}
						
						local coalitionName = signalGroup:getCoalitionName()
						if _signal.standby == false then
							for _name, _unit in pairs(signalCapabilityUnits) do

								if _unit:getCoalitionName() == coalitionName then
									if _unit:inAir() == true then
										local v = ATME.C_Vector3D(firstUnit:getPosition(), _unit:getPosition())
										if  v:getHModule() <= _signal.radius and 
										    ((signalGroup:hasPickupZone() == true and signalGroup:isReadyToBoard()) or signalGroup:hasPickupZone() == false) then
											
											table.insert(unitNamesInZone, _name)
										end
									end
								end
							end
							
							if #unitNamesInZone > 0 then
								if signalGroup:hasPickupZone() == true  then
									for _, _group in pickupZones[signalGroup:getPickupZoneName()].groups:pairs() do
										local group = ATME.C_Group.getByName(_group.groupName)
										
										if group ~= nil and signalGroup ~= group then
											local currentLocalMembers = C_Group_MT:get(group:getIndex())
											if currentLocalMembers.signalIndex > 0 then
												local currentSignal = signals:get(currentLocalMembers.signalIndex)
												currentSignal.standby = true
											end
										end
									end
								end

								local name = unitNamesInZone[math.random(1, #unitNamesInZone)]
								local unit = allAIUnitsAndPlayers[name]
								
								signalGroup:stop()
								_signal.unitName = name

								if _signal.type == "SIGNAL_SMOKE" and 
									(_signal.timeSmoke < 0 or _signal.timeSmoke + maxTimeSmoke <= currentTime) then
									firstUnit:fireSmoke(_signal.color)
									_signal.offsetTime = math.random(10,90)
								elseif _signal.type == "SIGNAL_FLARE" then
									firstUnit:fireFlare(_signal.color)
									_signal.offsetTime = math.random(60,120)
								end
								
								_signal.timeStart = currentTime
								_signal.timeSmoke = currentTime
								
								-- Evénement signal enclenché par le groupe si l'unité entre de sa zone de surveillance
								-- On ne stocke que les noms. La vérification de l'existance de l'unité et du groupe ne se fera que lors
								-- de l'activation de l'événement

								local event = {
									type = "SIGNAL_UNIT_IN_ZONE",
									datas = {
										unitName = name,
										linkedGroupName = _signal.groupName,
									},
								}
								
								C_EventsMgr_addCoreEvent(event)
							end
						end
					else
						local unit = allAIUnitsAndPlayers[_signal.unitName]
						
						if unit ~= nil then
							local v = ATME.C_Vector3D(firstUnit:getPosition(), unit:getPosition())

							if v:getHModule() <= _signal.radius then
								if _signal.timeSmoke + maxTimeSmoke + _signal.offsetTime <= currentTime and _signal.type == "SIGNAL_SMOKE" then
									firstUnit:fireSmoke(_signal.color)
									_signal.timeSmoke = currentTime
									_signal.offsetTime = math.random(10,90)
								elseif _signal.timeStart + _signal.offsetTime <= currentTime and _signal.type == "SIGNAL_FLARE" then
									firstUnit:fireFlare(_signal.color)
									_signal.offsetTime = math.random(60,120)
								end
							else
								-- Evénement signal enclenché par le groupe si l'unité sort de sa zone de surveillance 
								-- On ne stocke que les noms. La vérification de l'existance de l'unité et du groupe ne se fera que lors
								-- de l'activation de l'événement
								
								_signal.timeStart = -1
								local event = {
									type = "SIGNAL_UNIT_OUT_OF_ZONE",
									datas = {
										unitName = _signal.unitName,
										linkedGroupName = signalGroup:getName(),
									},
								}
								
								C_EventsMgr_addCoreEvent(event)
							end
						else
							_signal.timeStart = -1
						end
					end
				end
			end
			
			-- Contrôle des chargements d'unités dans des véhicules
			for _index, _groupToBoard in groupsToBoard:pairs() do
				-- DEBUG outputCore(nil, "Transport unit name = " .. _groupToBoard.transportUnitName .. " / toBoardGroupeName = " .. _groupToBoard.activeGroupName, 1)
				
				if allAIUnitsAndPlayers[_groupToBoard.transportUnitName] ~= nil then
					local transportUnit = allAIUnitsAndPlayers[_groupToBoard.transportUnitName]

					local transportUnitPosition = transportUnit:getPosition()
					local toBoardGroup = ATME.C_Group.getByName(_groupToBoard.activeGroupName)
					local localMembersTransportUnit
					
					if transportUnit:getClassName() == "C_Player" then
						localMembersTransportUnit = C_Player_MT:get(transportUnit:getIndex())
					else
						localMembersTransportUnit = C_AIUnit_MT:get(transportUnit:getIndex())
					end
					
					if toBoardGroup ~= nil then
						if transportUnit:isGroundVehicle() or ((transportUnit:isHelicopter() or transportUnit:isPlane()) and transportUnit:inAir() == false) then
							-- Vitesse des infanteries à 5 m/s soit un temps de parcours = dist/5
							-- Marge de sécurité à 10s en plus. Au delà, toute le groupe est chargé directement
							-- Ceci permet de contourner le problème d'une unité capricieuse qui louperait le transport et le rayon de chargement.
							
							if currentTime < _groupToBoard.timeStart + _groupToBoard.initialDistance / 5 + 10 then
								-- DEBUG outputCore(nil, "Transport unit name = " .. _groupToBoard.transportUnitName .. " au sol " .. _groupToBoard.initialDistance / 5, 1)
								
								for _, _unit in pairs(toBoardGroup:getUnits()) do
									local v = ATME.C_Vector3D(_unit:getPosition(), transportUnitPosition)
									local ok = ATME.whichSide(_groupToBoard.A, _groupToBoard.B, _unit:getPosition()) == _groupToBoard.sideOfWP2

									if v:getHModule() < personnelCarrierAttributes[transportUnit:getTypeName()].minDistance or ok == true then
										-- tant que l'unité de transport est au sol, les unités à transporter
										-- sont désactivées à l'approche de l'unité de transport. Distance paramétrée dans personnelCarrierAttributes
										table.insert(_groupToBoard.unitsOnBoard, _unit:getName())
										-- DEBUG outputCore(nil, "Unité " .. _unit:getName() .. " embarquée", 1)
										_unit:disable()
									end
								end
							else
								for _, _unit in pairs(toBoardGroup:getUnits()) do
									table.insert(_groupToBoard.unitsOnBoard, _unit:getName())
									_unit:disable()
								end
							end
						else
							if #_groupToBoard.unitsOnBoard > 0 then
								-- Si l'unité de transport décolle, tout le groupe est disable
								-- Embarquement en urgence ! des pertes sont possibles
								-- DEBUG outputCore(nil, "Urgence, transport " .. _groupToBoard.transportUnitName .. " décolle groupe " .. _groupToBoard.activeGroupName .. " embarqué", 1)
							
								local nb = 0

								for _, _unit in pairs(toBoardGroup:getUnits()) do
									if math.random(1,10) <= 7 - nb then
										table.insert(_groupToBoard.unitsOnBoard, _unit:getName())
										-- DEBUG outputCore(nil, "Unité " .. _unit:getName() .. " embarquée", 1)
									else 
										table.insert(_groupToBoard.unitsLost, _unit:getName())
										-- DEBUG outputCore(nil, "Unité " .. _unit:getName() .. " perdue", 1)
									end
									
									_unit:disable()
									
									if nb < 7 then
										nb = nb + 1
									end
								end
							else
								toBoardGroup:stop()
								local index = 0
								for _i, _groupItem in ipairs(localMembersTransportUnit.groupsBoarding) do
									if _groupToBoard.activeGroupName == _groupItem.groupName then
										index = _i
									end
								end
								
								if index > 0 then
									table.remove(localMembersTransportUnit.groupsBoarding, index)
								end
								
								_groupToBoard.unitsOnBoard = nil
								_groupToBoard.unitsLost = nil

								local localMembers = C_Group_MT:get(toBoardGroup:getIndex())
								localMembers.boardingStarted = false
								groupsToBoard:remove(_index)
							end
						end
					else
						-- Le groupe à transporter est inexistant (détruit ou disable complet)
						if #_groupToBoard.unitsOnBoard > 0 then
							-- DEBUG outputCore(nil, "Transport " .. _groupToBoard.transportUnitName .. " : " .. #_groupToBoard.unitsOnBoard .. " unités embarqués", 1)
							-- DEBUG outputCore(nil, "Transport " .. _groupToBoard.transportUnitName .. " : " .. #_groupToBoard.unitsLost .. " unités perdues", 1)
							
							-- On ne conserve que les unités ayant réussi à monter
							
							local groupDatas = C_GroupSpawnDatas_createFromMissionInfos(_groupToBoard.activeGroupName, 
																							_groupToBoard.origineMissionDatasGroupName, 
																							_groupToBoard.waypointsGroupName,
																							_groupToBoard.unitsOnBoard, 
																							nil)
							
							localMembersTransportUnit.nbUnitsOnBoard = localMembersTransportUnit.nbUnitsOnBoard + #_groupToBoard.unitsOnBoard
							
							local index = 0
							for _i, _groupItem in ipairs(localMembersTransportUnit.groupsBoarding) do
								if _groupToBoard.activeGroupName == _groupItem.groupName then
									index = _i
								end
							end
							
							if index > 0 then
								table.remove(localMembersTransportUnit.groupsBoarding, index)
							end
							
							-- DEBUG outputCore(nil, "Still boarding = " .. #localMembersTransportUnit.groupsBoarding, 1)
							
							local idGroupData = C_Player_C_AIUnit_addOnBoardGroup(transportUnit, groupDatas)

							-- Evénement de fin d'embarquement
							-- On ne stocke que le nom. La vérification de l'existance de l'unité ne se fera que lors
							-- de l'activation de l'événement. 
							-- ATTENTION : Ici name est le nom du group qui va être disable... Donc pas de réactivation
							-- à l'activation de l'événement.
							
							
							local event = {
								type = "TRANSPORT_END_OF_BOARDING",
								datas = {
									unitName = transportUnit:getName(),
									index = idGroupData,
									onBoardGroupName = groupDatas:getName(),
									nbOnBoardUnits = #_groupToBoard.unitsOnBoard,
									nbDeadUnits = #_groupToBoard.unitsLost,
								},
							}
							
							C_EventsMgr_addCoreEvent(event)
						end
						
						while #_groupToBoard.unitsOnBoard > 0 do
							table.remove(_groupToBoard.unitsOnBoard, #_groupToBoard.unitsOnBoard)
						end
						
						_groupToBoard.unitsOnBoard = nil
						_groupToBoard.unitsLost = nil
						groupsToBoard:remove(_index)
					end
				else
					-- L'unité de transport est inexistante (détruit ou changement de slot)
					-- Tout est perdu. Seules restent les unités du groupe extérieures à l'unité de transport.
					
					while #_groupToBoard.unitsOnBoard > 0 do
						table.remove(_groupToBoard.unitsOnBoard, #_groupToBoard.unitsOnBoard)
					end
					
					while #_groupToBoard.unitsLost > 0 do
						table.remove(_groupToBoard.unitsLost, #_groupToBoard.unitsLost)
					end
					
					_groupToBoard.unitsOnBoard = nil
					_groupToBoard.unitsLost = nil
					local toBoardGroup = ATME.C_Group.getByName(_groupToBoard.activeGroupName)
					if toBoardGroup ~= nil then
						local localMembers = C_Group_MT:get(toBoardGroup:getIndex())
						localMembers.boardingStarted = false
					end
					groupsToBoard:remove(_index)
				end
			end
			
			-- Vérification de l'existance des unités & groupes liés aux core events et
			-- récupération de l'unité à partir de son nom sinon suppression
			local localEventsMgrMembers = C_EventsMgr_MT:get(eventsMgr[activeEventsMgr]:getIndex())
			for _id, _ in pairs(localEventsMgrMembers.list) do  
				-- _id toujours un number car là pas encore de userTrigger events ajouté
				local unitName = localEventsMgrMembers.list[_id].datas.unitName
				
				if allAIUnitsAndPlayers[unitName] == nil then
					localEventsMgrMembers.list[_id] = nil
					localEventsMgrMembers.nbCoreEvents = localEventsMgrMembers.nbCoreEvents - 1
				else
					localEventsMgrMembers.list[_id].datas.unit = allAIUnitsAndPlayers[unitName]
					
					if type(localEventsMgrMembers.list[_id].datas.linkedGroupName) == "string" then
						if groups[localEventsMgrMembers.list[_id].datas.linkedGroupName] ~= nil then
							-- Traitement des cas type TRANSPORT_END_OF_DISEMBARK, SIGNAL_UNIT_IN_ZONE et SIGNAL_UNIT_OUT_OF_ZONE
							-- Associe le groupe à partir de son nom si il existe toujours au moment du traitement.
						
							localEventsMgrMembers.list[_id].datas.linkedGroup = groups[localEventsMgrMembers.list[_id].datas.linkedGroupName]
						else
							localEventsMgrMembers.list[_id] = nil
							localEventsMgrMembers.nbCoreEvents = localEventsMgrMembers.nbCoreEvents - 1
						end
					end
				end
			end
			
			-- Mise à jour du nombre de joueurs
			local nbPlayers = 0
			for _, _player in pairs(players) do
				nbPlayers = nbPlayers + 1
			end
			
			if oldNbPlayers ~= nbPlayers then
				outputCore(nil,"Nb players = " .. nbPlayers, 2)
				oldNbPlayers = nbPlayers
			end
			
			-- On fige les événements qui vont être générés et émis aux handlers.
			-- On bascule activeEvents sur le nouveau pour prendre en compte au prochain coup les événements
			-- éventuellement générés par ces mêmes handlers (OnTimer et OnUpdatePlayer)
			local fixedEventsMgr = activeEventsMgr
			activeEventsMgr = 1 - activeEventsMgr
			
			-- Ajoute à la liste des core events les événements enclenchés par les user triggers
			for _, _localModuleMembers in C_Module_MT:pairs() do
				for _name, _currentEvent in pairs(_localModuleMembers.userTriggerEvents) do
					local index = _currentEvent:getIndex()
					local localMembers = C_UserTrigger_MT:get(index)
					local isAlreadyActivated = _currentEvent:isActivated()
					if _currentEvent:getFlag() ~= -1 and ATME.getFlag(_currentEvent:getFlag()) > 0 and _currentEvent:isArmed() == false then
						localMembers.timeStart = localMembers.timeStart + currentTipsCount
						localMembers.timeEnd = localMembers.timeEnd + currentTipsCount
						localMembers.armed = true
						outputCore(nil,"Module " .. _localModuleMembers.name .. " - Trigger " .. _currentEvent:getName() .. " armé sur flag" , 2)
					end
					
					if _currentEvent:getTimeStart() <= currentTipsCount and _currentEvent:getTimeEnd() >= currentTipsCount and _currentEvent:isArmed() == true then
						localMembers.activated = true
						outputCore(nil,"Module " .. _localModuleMembers.name .. " - Trigger " .. _currentEvent:getName() .. " déclenché" , 2)
					end
					
					if _currentEvent:isActivated() == true then
						C_EventsMgr_addTriggerUserEvents(fixedEventsMgr, _name, isAlreadyActivated == false)
					end

					if _currentEvent:getTimeEnd() <= currentTipsCount and _currentEvent:isArmed() == true then 
						localMembers.activated = false
						_localModuleMembers.userTriggerEvents[_name] = nil
						outputCore(nil,"Module " .. _localModuleMembers.name .. " - Trigger " .. _currentEvent:getName() .. " arrêté" , 2)
					end
				end
				
				if _localModuleMembers.handlers.onTimerHandler ~= nil then
					outputCore(nil,"Appel onTimerHandler du module " .. _localModuleMembers.name, 2)
					_localModuleMembers.handlers.onTimerHandler(eventsMgr[fixedEventsMgr])
					outputCore(nil,"Retour onTimerHandler du module " .. _localModuleMembers.name, 2)
				end
				
				for _, _player in pairs(players) do
					if _localModuleMembers.handlers.onUpdatePlayerHandler ~= nil then
						-- Appel du module
						outputCore(nil,"Appel onUpdatePlayerHandler du module " .. _localModuleMembers.name, 2)
						_localModuleMembers.handlers.onUpdatePlayerHandler(_player, eventsMgr[fixedEventsMgr])
						outputCore(nil,"Retour onUpdatePlayerHandler du module " .. _localModuleMembers.name, 2)
					end
				end
				
				C_EventsMgr_removeAllTriggerUserEvents(fixedEventsMgr)
			end

			C_EventsMgr_removeAll(fixedEventsMgr)
			
			-- DEBUG outputCore(nil,"Update main timer", 1)
			timer.scheduleFunction(_timerMainFunction, {}, ATME.getTime() + 1)
		end
	end

	----------------------------------------------------------------------------------------------------------
	-- Fonction _timer100x est appelée à l'initialisation voir plus bas puis réarmée tous les 1/100è de seconde.
	-- Cette fonction permet de mesurer les temps au 1/100è de seconde. Elle déclenche les events ATME :
	-- 	RACE_START : quand un joueur de la course franchit le départ
	--	RACE_TURN_TIME : quand un joueur boucle un tour par un nouveau franchissement de la ligne de départ,
	--					 pour les courses à plusieurs tours
	--	RACE_PLAYER_BEST_TURN_TIME : Meilleur temps au tour du joueur qui boucle son tour
	-- 	RACE_BEST_TURN_TIME : Record du tour pour la course depuis le début de la mission
	-- 	RACE_LAP_TIME : Temps intermédiaire pour le joueur franchissant une porte définie.
	--	RACE_BEST_LAP_TIME : Meilleur temps intermédiaire pour une porte définie depuis le début de la mission
	--	RACE_DOOR_MISSED : Le joueur vient de louper une porte (la porte de départ n'est pas prise en compte.
	--					   La porte de départ correspond à la première porte du premier tour, soit le départ de
	--					   la course.
	-- 	RACE_PLAYER_TOO_HIGH : Le joueur a dépassé l'altitude maximale autorisée
	--	RACE_FINAL_TIME : Temps final pour la course pour le joueur venant de franchir l'arrivée (dernière
	--					  porte de la course).
	--	RACE_PLAYER_BEST_FINAL_TIME : Meilleur temps final pour le joueur venant de franchir l'arrivée
	--	RACE_BEST_FINAL_TIME : Meilleur temps final depuis le début de la mission. Le classement général est
	--						   recalculé à partir de ce temps.
	--
	-- Le passage d'une porte est pris en compte que si la porte précédente a été passée (franchie ou loupée)
	--
	-- ATTENTION : Il y a un décallage de temps entre la création du cor event et l'activation vers les modules.
	-- 			   On ne stocke que le nom dde l'unité joueur. La vérification de l'existance de l'unité ne se 
	-- 			   fera que lors de l'activation de l'événement dans MainTimer.
	--			   En effet, l'unité peut être détruite entre temps.
	----------------------------------------------------------------------------------------------------------
	local _timer100x
	_timer100x = function()
		if exitATME == false then
			-- Fonction des mises à jour de la liste des joueurs ... 
			-- Important pour éviter les incohérences, les timers étant asynchrones
			updateCurrentPlayers()
			
			local currentTime = ATME.getTime()
			for _, _race in pairs(races) do
				local localMembers = C_Race_MT:get(_race:getIndex())
				local maxAltitude = _race:getMaxAltitude()
				for _rowBestPlayer, _item in ipairs(localMembers.bestPlayers) do
					local racePlayer = localMembers.players[_item.player:getName()]
					
					local leftSide, rightSide = _race:getDoorSides(_race:getDoorIndex(racePlayer.nextDoor))
					local leftPosition = leftSide:getPosition()
					local rightPosition = rightSide:getPosition()
					
					local playerPosition = racePlayer.player:getPosition()
					local v1 = ATME.C_Vector3D(leftPosition, rightPosition)
					local v2 = ATME.C_Vector3D(leftPosition, playerPosition)
					local v3 = ATME.C_Vector3D(rightPosition, playerPosition)
					
					local okSegment = false
					local okAxis = false
					
					if v2:getHModule() <= v1:getHModule() and v3:getHModule() <= v1:getHModule() then
						okSegment = true
					end
					
					local side = ATME.whichSide(leftPosition, rightPosition, playerPosition)
					if racePlayer.oldSide ~= nil and racePlayer.oldSide ~= side and side == -1 then
						okAxis = true
					end
					
					if okAxis == true then
						-- Axe franchi
						
						if okSegment == true then
							-- Porte franchie
							-- DEBUG outputCore(nil, "Porte franchie", 1)
							if racePlayer.nextDoor == 1 and racePlayer.currentTurn == 1 then
								if racePlayer.player:getAGLAltitude() <= maxAltitude or maxAltitude == -1 then
								-- Franchissement de la ligne de départ
								
								racePlayer.startTime = currentTime
								racePlayer.startTurnTime = currentTime
								racePlayer.penalty = 0
								
								C_Race_createEvent(_race, 
									"RACE_START", 
									{
										unitName = racePlayer.player:getName(),
									})
								else
									racePlayer.nextDoor = 0
								end
							elseif racePlayer.player:getAGLAltitude() > maxAltitude and maxAltitude ~= -1 then
								-- Test de l'altitude si on est en course (nextDoors <> 1 ou currentTurn <> 1)

								racePlayer.penalty = racePlayer.penalty + _race:getMaxAltitudePenalty()

								C_Race_createEvent(_race, 
									"RACE_PLAYER_TOO_HIGH",
									{
										unitName = racePlayer.player:getName(),
										penalty = racePlayer.penalty,
									})
							end
						else 
							-- Passage sur les côtés de la porte
							
							if racePlayer.nextDoor == 1 and racePlayer.currentTurn == 1 then
								racePlayer.nextDoor = 0
							else
								racePlayer.penalty = racePlayer.penalty + _race:getMissedDoorPenalty()

								C_Race_createEvent(_race, 
									"RACE_DOOR_MISSED", 
									{
										unitName = racePlayer.player:getName(),
										doorNumber = racePlayer.nextDoor,
									})
							end
						end
						
						local totalPlayTime = currentTime - racePlayer.startTime + racePlayer.penalty
						
						if racePlayer.nextDoor == 1 and racePlayer.currentTurn > 1 then
							-- Franchissement de la ligne de départ et bouclage d'un tour
							local playTime = currentTime - racePlayer.startTurnTime + racePlayer.penalty
							C_Race_createEvent(_race, 
									"RACE_TURN_TIME", 
									{
										unitName = racePlayer.player:getName(),
										playTime = playTime,
										totalPlayTime = totalPlayTime,
										penalty = racePlayer.penalty,
										turnNumber = racePlayer.currentTurn - 1,
									})
							
							if playTime < racePlayer.bestTurnTime or racePlayer.bestTurnTime < 0 then
								racePlayer.bestTurnTime = playTime
								C_Race_createEvent(_race, 
									"RACE_PLAYER_BEST_TURN_TIME",
									{
										unitName = racePlayer.player:getName(),
										playTime = playTime,
										turnNumber = racePlayer.currentTurn - 1
									})
							end
							
							if playTime < localMembers.bestTurn.bestTime or localMembers.bestTurn.bestTime < 0 then
								localMembers.bestTurn.bestTime = playTime
								localMembers.bestTurn.pseudo = racePlayer.player:getPseudo()
								C_Race_createEvent(_race, 
									"RACE_BEST_TURN_TIME",
									{
										unitName = racePlayer.player:getName(),
										playTime = playTime,
										turnNumber = racePlayer.currentTurn - 1
									})
							end

							racePlayer.startTurnTime = currentTime + racePlayer.penalty
						elseif _race:getLapAtDoor(_race:getDoorIndex(racePlayer.nextDoor)) == true then
							-- Portes avec temps intermédiaire
							local door = localMembers.doorsIndex[_race:getDoorIndex(racePlayer.nextDoor)]
							local playTime = currentTime - racePlayer.startTurnTime + racePlayer.penalty
							
							C_Race_createEvent(_race, 
								"RACE_LAP_TIME", 
								{
									unitName = racePlayer.player:getName(),
									doorNumber = racePlayer.nextDoor,
									playTime = playTime,
									totalPlayTime = totalPlayTime,
									penalty = racePlayer.penalty,
									turnNumber = racePlayer.currentTurn - 1																		
								})
								
							if playTime < door.bestTurn.bestTime or door.bestTurn.bestTime < 0 then
								door.bestTurn.bestTime = playTime
								door.bestTurn.pseudo = racePlayer.player:getPseudo()
								C_Race_createEvent(_race, 
									"RACE_BEST_LAP_TIME", 
									{
										unitName = racePlayer.player:getName(),
										doorNumber = racePlayer.nextDoor,
										playTime = playTime, 
										turnNumber = racePlayer.currentTurn - 1
									})
							end
						elseif racePlayer.nextDoor == _race:getNbDoors() then
							-- Franchissement de la ligne d'arrivée
							racePlayer.nextDoor = 0
							
							if racePlayer.currentTurn == _race:getNbTurns() then
								if totalPlayTime < racePlayer.bestTime or racePlayer.bestTime < 0 then
										racePlayer.bestTime = totalPlayTime
										C_Race_createEvent(_race, 
											"RACE_PLAYER_BEST_FINAL_TIME", 
											{
												unitName = racePlayer.player:getName(),
												totalPlayTime = totalPlayTime,
											})
								end
								
								if totalPlayTime < _item.bestTime or _item.bestTime < 0 then
									_item.bestTime = totalPlayTime
									_item.recordTime = currentTime
									
									C_Race_createEvent(_race, 
										"RACE_BEST_FINAL_TIME",
										{
											unitName = racePlayer.player:getName(),
											totalPlayTime = totalPlayTime, 
										})
								end
								
								C_Race_createEvent(_race, 
									"RACE_FINAL_TIME", 
									{
										unitName = racePlayer.player:getName(),
										totalPlayTime = totalPlayTime,
										penalty = racePlayer.penalty,
									})
								
								racePlayer.currentTurn = 0
							end
							
							racePlayer.currentTurn = racePlayer.currentTurn + 1
						end
						
						racePlayer.nextDoor = racePlayer.nextDoor + 1
					end
					
					racePlayer.oldSide = side
				end
				
				table.sort(localMembers.bestPlayers, 
								function(a, b)
									if a.bestTime >= 0 and b.bestTime >= 0 and a.bestTime ~= b.bestTime then
										return a.bestTime < b.bestTime
									elseif a.bestTime < 0 and b.bestTime >= 0 then
										return false
									elseif a.bestTime >=0 and b.bestTime < 0 then
										return true
									else
										return a.recordTime < b.recordTime
									end
								end)
			end
			-- DEBUG outputCore(nil, "Current time = " .. currentTime, 1)
			-- DEBUG Penser à baisser la fréquence si activation affichage
			timer.scheduleFunction(_timer100x, {}, ATME.getTime() + 0.01)
		end
	end
	
	
	----------------------------------------------------------------------------------------------------------
	-- Fonction gestion événements DCS ... essentielle ... appelée si l'événement se produit
	-- En l'état ne gère pas tous les événements mais uniquement ceux nécessaires
	----------------------------------------------------------------------------------------------------------	
	function customEventHandler:onEvent(_event)
		if exitATME == true then
			world.removeEventHandler(customEventHandler)
		else
			outputCore(nil,"Event " .. _event.id .. " déclenché", 2)
			if world.event.S_EVENT_BIRTH  == _event.id then
				-- Si une unité est spawnée en cours de mission, on passe ici
				-- Valable aussi si c'est un joueur qui arrive ou change de slot après le début de mission
				
				local unitName = _event.initiator:getName()
				
				if ATME.isUnitControllable(unitName) then -- Player unit
					if ATME.C_Player.exists(unitName) == true then
						C_Player_delete(players[unitName])
					end
					
					C_Player_create(_event.initiator)
					outputCore(nil,"Player " .. unitName .. " créé", 2)
				elseif ATME.isStaticObject(unitName) == true then
					if ATME.C_StaticObject.exists(unitName) == true then
						C_StaticObject_delete(staticObjects[unitName])
					end
					
					C_StaticObject_create(_event.initiator)
					outputCore(nil,"StaticUnit " .. unitName .. " créée", 2)
				else
					local unit = ATME.C_AIUnit.getByName(unitName)
					local isLateActivation = false
					if unit ~= nil then
						-- On verifie s'il ne s'agit pas d'une unité dont le groupe est à activation retardée
						local group = unit:getGroup()
						local groupLocalMembers = C_Group_MT:get(group:getIndex())
						
						if groupLocalMembers.isEnabled == true then
							C_AIUnit_delete(unit)
						else
							isLateActivation = true
							groupLocalMembers.isEnabled = true
							outputCore(nil,"Activation du groupe " .. group:getName(), 2)
						end
					end
					
					-- L'événement survient pendant le spawn ...
					-- ou lors de l'activation retardée d'une unité ... on ne traite que le spawn
					-- Depuis la version 1.5.5, parfois le groupe n'est pas encore initialisé
					-- On mémorise donc les unités créées qui seront gérées par ATME après le spawn.
					
					if isLateActivation == false then
						table.insert(unitsReadyToCreate, _event.initiator)
					end
				end
			elseif world.event.S_EVENT_DEAD  == _event.id  or world.event.S_EVENT_PILOT_DEAD  == _event.id then
				local unitName = _event.initiator:getName()
				
				if ATME.C_AIUnit.exists(unitName) == true then
					C_AIUnit_delete(AIUnits[unitName])
					outputCore(nil,"AIUnit " .. unitName .. " détruite", 2)
				elseif ATME.C_Player.exists(unitName) == true then
					C_Player_delete(players[unitName])
					outputCore(nil,"Player " .. unitName .. " mort", 2)
				elseif ATME.C_StaticObject.exists(unitName) == true then
					C_StaticObject_delete(staticObjects[unitName])
					outputCore(nil,"StaticUnit " .. unitName .. " détruite", 2)
				end
			elseif world.event.S_EVENT_TAKEOFF == _event.id then
				local unitName = _event.initiator:getName()
				if ATME.C_Player.exists(unitName) == true then
					local player = players[unitName]

					for _, _localModuleMembers in C_Module_MT:pairs() do
						if _localModuleMembers.handlers.onTakeoffPlayerHandler ~= nil then
							-- Appel du module
							outputCore(nil,"Appel onTakeoffPlayerHandler du module " .. _localModuleMembers.name, 2)
							_localModuleMembers.handlers.onTakeoffPlayerHandler(players[unitName])
							outputCore(nil,"Retour onTakeoffPlayerHandler du module " .. _localModuleMembers.name, 2)
						end
					end
					outputCore(nil,"Player " .. unitName .. " décollage", 2)
				elseif ATME.C_AIUnit.exists(unitName) == true then
					for _, _localModuleMembers in C_Module_MT:pairs() do
						if _localModuleMembers.handlers.onTakeoffAIUnitHandler ~= nil then
							-- Appel du module
							outputCore(nil,"Appel onTakeoffAIUnitHandler du module " .. _localModuleMembers.name, 2)
							_localModuleMembers.handlers.onTakeoffAIUnitHandler(AIUnits[unitName])
							outputCore(nil,"Retour onTakeoffAIUnitHandler du module " .. _localModuleMembers.name, 2)
						end
					end
					outputCore(nil,"AIUnit " .. unitName .. " décollage", 2)
				end
			elseif world.event.S_EVENT_LAND == _event.id then
				local unitName = _event.initiator:getName()
				if ATME.C_Player.exists(unitName) == true then
					for _, _localModuleMembers in C_Module_MT:pairs() do
						if _localModuleMembers.handlers.onLandingPlayerHandler ~= nil then
							-- Appel du module
							outputCore(nil,"Appel onLandingPlayerHandler du module " .._localModuleMembers.name, 2)
							_localModuleMembers.handlers.onLandingPlayerHandler(players[unitName])
							outputCore(nil,"Retour onLandingPlayerHandler du module " .. _localModuleMembers.name, 2)
						end
					end
					outputCore(nil,"Player " .. unitName .. " atterri", 2)
				elseif ATME.C_AIUnit.exists(unitName) == true then
					for _, _localModuleMembers in C_Module_MT:pairs() do
						if _localModuleMembers.handlers.onLandingAIUnitHandler ~= nil then
							-- Appel du module
							outputCore(nil,"Appel onLandingAIUnitHandler du module " .. _localModuleMembers.name, 2)
							_localModuleMembers.handlers.onLandingAIUnitHandler(AIUnits[unitName])
							outputCore(nil,"Retour onLandingAIUnitHandler du module " .. _localModuleMembers.name, 2)
						end
					end
					outputCore(nil,"AIUnit " .. unitName .. " atterri", 2)
				end
			elseif world.event.S_EVENT_ENGINE_STARTUP  == _event.id then
				local unitName = _event.initiator:getName()
				if ATME.C_Player.exists(unitName) == true then
					for _, _localModuleMembers in C_Module_MT:pairs() do
						if _localModuleMembers.handlers.onStartEnginePlayerHandler ~= nil then
							-- Appel du module
							outputCore(nil,"Appel onStartEnginePlayerHandler du module " .. _localModuleMembers.name, 2)
							_localModuleMembers.handlers.onStartEnginePlayerHandler(players[unitName])
							outputCore(nil,"Retour onStartEnginePlayerHandler du module " .. _localModuleMembers.name, 2)
						end
					end
					
					local localMembers = C_Player_MT:get(players[unitName]:getIndex())
					localMembers.engineOn = true
					outputCore(nil,"Player " .. unitName .. " mise en route moteurs", 2)
				elseif ATME.C_AIUnit.exists(unitName) == true then
					for _, _localModuleMembers in C_Module_MT:pairs() do
						if _localModuleMembers.handlers.onStartEngineAIUnitHandler ~= nil then
							-- Appel du module
							outputCore(nil,"Appel onStartEngineAIUnitHandler du module " .. _localModuleMembers.name, 2)
							_localModuleMembers.handlers.onStartEngineAIUnitHandler(AIUnits[unitName])
							outputCore(nil,"Retour onStartEngineAIUnitHandler du module " .. _localModuleMembers.name, 2)
						end
					end
					outputCore(nil,"AIUnit " .. unitName .. " mise en route moteurs", 2)
				end
			elseif world.event.   S_EVENT_ENGINE_SHUTDOWN    == _event.id then
				local unitName = _event.initiator:getName()
				if ATME.C_Player.exists(unitName) == true then
					for _, _localModuleMembers in C_Module_MT:pairs() do
						if _localModuleMembers.handlers.onStopEnginePlayerHandler ~= nil then
							-- Appel du module
							outputCore(nil,"Appel onStopEnginePlayerHandler du module " .. _localModuleMembers.name, 2)
							_localModuleMembers.handlers.onStopEnginePlayerHandler(players[unitName])
							outputCore(nil,"Retour onStopEnginePlayerHandler du module " .. _localModuleMembers.name, 2)
						end
					end
					local localMembers = C_Player_MT:get(players[unitName]:getIndex())
					localMembers.engineOn = false
					
					outputCore(nil,"Player " .. unitName .. " arrêt moteurs", 2)
				elseif ATME.C_AIUnit.exists(unitName) == true then
					for _, _localModuleMembers in C_Module_MT:pairs() do
						if _localModuleMembers.handlers.onStopEngineAIUnitHandler ~= nil then
							-- Appel du module
							outputCore(nil,"Appel onStopEngineAIUnitHandler du module " .. _localModuleMembers.name, 2)
							_localModuleMembers.handlers.onStopEngineAIUnitHandler(AIUnits[unitName])
							outputCore(nil,"Retour onStopEngineAIUnitHandler du module " .. _localModuleMembers.name, 2)
						end
					end
					outputCore(nil,"AIUnit " .. unitName .. " arrêt moteurs", 2)
				end
			end
		end
	end
	
	----------------------------------------------------------------------------------------------------------
	-- Fonction d'initialisation des objets définis dans la mission
	-- Adapté de MIST - MIssion Scripting Tools
	-- Complétée par la recherche des FARP et AirFields nécessaires à la gestion des airbases.
	--¨Permet notamment de récupérer le groupId pour les joueurs en multijoueur, utilisé ensuite des les fonctions
	-- DCS associées (BUG DCS sur le Group.getId d'un joueur en multijoueur).
	----------------------------------------------------------------------------------------------------------
	local function initObjectsInMission()	
		for _coalitionName, _coalitionDatas in pairs(env.mission.coalition) do
			if (_coalitionName == 'red' or _coalitionName == 'blue') and type(_coalitionDatas) == 'table' then
				if _coalitionDatas.country then
					for _country_id, _countryDatas in pairs(_coalitionDatas.country) do
						for _typeName, _datas in pairs(_countryDatas) do
							if _typeName == "helicopter" or _typeName == "ship" or _typeName == "plane" or _typeName == "vehicle" or _typeName == "static" then	
								local isStatic = _typeName == "static"
								for _, _groupDatas in pairs(_datas.group) do
									if _groupDatas and _groupDatas.units then
										local currentGroupName = env.getValueDictByKey(_groupDatas.name)
										if  _groupDatas.groupId > maxGroupId then
											maxGroupId = _groupDatas.groupId
										end
										
										if isStatic == false then
											allGroupsInMission[currentGroupName] = {
												name = currentGroupName,
												country = _countryDatas.id,
												groupId = _groupDatas.groupId,
												category = groupCategories[_typeName],
												type = _typeName,
												units = {},
												visible = false,
												taskSelected = true,
												lateActivation = _groupDatas.lateActivation,
												tasks = {},
												hidden = _groupDatas.hidden,
												start_time = 0,
												task = "Ground Nothing",
												waypoints = {}
											}
												
											for _, _unitDatas in pairs(_groupDatas.units) do
												local unitName = env.getValueDictByKey(_unitDatas.name)
												
												allGroupsInMission[currentGroupName].units[unitName] = {
													name = unitName,
													type = _unitDatas.type,
													skill = _unitDatas.skill,
													x = _unitDatas.x,
													y = _unitDatas.y,
													playerCanDrive = _unitDatas.playerCanDrive,
													heading = _unitDatas.heading,
													alt = _unitDatas.alt,
													altType = _unitDatas.alt_type,
												}
												
												if _unitDatas.skill == "Client" or _unitDatas.skill == "Player" then
													allPlayerUnitsInMission[unitName] = {
														group = allGroupsInMission[currentGroupName],
													}
												else
													allAIUnitsInMission[unitName] = {
														group = allGroupsInMission[currentGroupName],
														unit = allGroupsInMission[currentGroupName].units[unitName],
													}
												end
											end
											
											if _groupDatas.route and _groupDatas.route.points and #_groupDatas.route.points > 0 then
												for _pointNum, _point in ipairs(_groupDatas.route.points) do
													local pointName =  env.getValueDictByKey(_point.name)
													allGroupsInMission[currentGroupName].waypoints[#allGroupsInMission[currentGroupName].waypoints + 1] = {
														name = pointName,
														x = _point.x,
														y = _point.y,
														speed = _point.speed,
														alt = _point.alt,
														alt_type = _point.alt_type,
														ETA = _point.ETA,
														ETA_locked = _point.ETA_locked,
														airdromeId = _point.airdromeId,
														helipadId = _point.helipadId,
														type = _point.type,
														action = _point.action,
														task = _point.task,
													}
												end
											end
										else 
											for _, _unitDatas in pairs(_groupDatas.units) do
												local unitName = env.getValueDictByKey(_unitDatas.name)
												if _unitDatas.category ~= "GrassAirfields" and _unitDatas.category ~= "Heliports" then
													allStaticObjectsInMission[unitName] = {
														name = unitName,
														type = _unitDatas.type,
														x = _unitDatas.x,
														y = _unitDatas.y,
														canCargo = _unitDatas.canCargo,
														heading = _unitDatas.heading,
														category = _unitDatas.category,
														dead = _groupDatas.dead,
														shape_name = _unitDatas.shape_name,
														mass = _unitDatas.mass,
														rate =_unitDatas.rate,
														livery_id = _unitDatas.livery_id,
													}
												else
													allStaticAirbasesInMission[unitName] = {
														name = unitName,
														type = _unitDatas.type,
														id = _unitDatas.unitId,
														x = _unitDatas.x,
														y = _unitDatas.y,
														canCargo = _unitDatas.canCargo,
														heading = _unitDatas.heading,
														category = _unitDatas.category,
														dead = _groupDatas.dead,
													}
												end
											end
										end
									end
								end 
							end
						end
					end
				end
			end
		end
	end
	
	----------------------------------------------------------------------------------------------------------
	-- Fonction d'initialisation des airBases
	----------------------------------------------------------------------------------------------------------
	local function initAirbases()
		for _, _current in ipairs(world.getAirbases()) do
			local id = _current:getID()
			
			-- Attention Bug DCS : getID est un nombre pour les bases de la cartes, et un string pour les bases créées de la mission.
			-- De plus, pour un FARP, il n'existe dans cette liste que si un hélico est posé dessus en début de mission.
			-- Sinon rien, donc nécessité de filtrer ici pour gérer ensuite par allStaticAirbasesInMission
			
			if type(_current:getID() ) == "number" then
				C_Airbase_create(_current:getName(),_current:getID())
			end
		end
		
		for _, _current in pairs(allStaticAirbasesInMission) do
			C_Airbase_create_helipad_airField(_current.name, _current.id)
		end
	end
	
	----------------------------------------------------------------------------------------------------------
	-- Fonction de démmarrage d'ATME à faire après le chargement de tous les modules. Cette fonction se mets dans
	-- l'éditeur de mission par un script lua.
	----------------------------------------------------------------------------------------------------------
	function ATME.run(_language)
		if type(_language) == "string" and supportedLanguages[_language] ~= nil then
			language = _language
		end
		
		initAirbases()

		for _, _object in pairs(allStaticObjectsInMission) do
			local objectName = _object.name
			
			local object = StaticObject.getByName(objectName)
			C_StaticObject_create(object)
		end

		for _, _localModuleMembers in C_Module_MT:pairs() do
			if _localModuleMembers.handlers.onModuleStartHandler ~= nil then
				-- Appel du module
				outputCore(nil,"Appel onModuleStartHandler avant initialisation des unités & joueurs du module " .. _localModuleMembers.name, 2)
				_localModuleMembers.handlers.onModuleStartHandler(false)
				outputCore(nil,"Retour onModuleStartHandler du module " .. _localModuleMembers.name, 2)
			end
		end

		world.addEventHandler(customEventHandler) -- Les événements doivent être activé avant le deuxième appel à onModuleStartHandler
		
		-- On notera que les unités avec une activation retardées sont bien créées ! 
		-- ATTENTION ... Vrai pour les unités joueurs et les unités AI
		for _, _group in pairs(allGroupsInMission) do
			for _, _unit in pairs(_group.units) do
				local unitName = _unit.name
				local unit = Unit.getByName(unitName)
				if ATME.isUnitControllable(unitName) == false and unit ~= nil then
					C_AIUnit_create(unit)
				end
			end
			
			if _group.lateActivation == true then
				local group = ATME.C_Group.getByName(_group.name)
				local currentLocalMembers = C_Group_MT:get(group:getIndex())
				currentLocalMembers.isEnabled = false
				outputCore(nil,"Activation retardee pour " .. _group.name, 2)
			end
		end
		
		for i = 0, 2 do
			local newPlayerUnits = coalition.getPlayers(i)
			for _, _newPlayer in ipairs(newPlayerUnits) do
				C_Player_create(_newPlayer)
			end
		end
	
		isRunning = true
		
		
		for _, _localModuleMembers in C_Module_MT:pairs() do
			if _localModuleMembers.handlers.onModuleStartHandler ~= nil then
				-- Appel du module
				outputCore(nil,"Appel onModuleStartHandler après initialisation des unités & joueurs du module " .. _localModuleMembers.name, 2)
				_localModuleMembers.handlers.onModuleStartHandler(true)
				outputCore(nil,"Retour onModuleStartHandler du module " .. _localModuleMembers.name, 2)
			end
		end
		
		
		_timerMainFunction()
		_timer100x()
	end
	
	
	----------------------------------------------------------------------------------------------------------
	-- Fonction MAIN d'initialisation générale appelée une seule fois au lancement du fichier script
	----------------------------------------------------------------------------------------------------------
	do
		initObjectsInMission()
		theatreName = string.upper(env.mission.theatre)
	end
end