//! The object which is in a trigger and its metadata class TriggerInsider { ref OLinkT insider; // DEPRECATED //! Object that data belongs to protected Object m_Object; //! Last time the object was seen in ms int timeStamp; //! Time the object was first seen in seconds float timeEntered; //! Last time the object was updated in seconds, is used for calculating deltaTime float lastUpdated; void TriggerInsider(Object obj) { insider = new OLinkT(obj); m_Object = obj; } void ~TriggerInsider() { } Object GetObject() { return m_Object; } }; #ifdef DIAG_DEVELOPER typedef Param7> DebugTriggerInfo; #endif //! Scripted Trigger class Trigger : TriggerEvents { //! DEPRECATED const int TIMEOUT = 1000; //! The objects and their metadata which are currently inside the Trigger ref array m_insiders; #ifdef DIAG_DEVELOPER bool m_Local;//is this trigger spawning on client only ? string m_DebugAreaType; ref array m_dbgInsiders; #endif //! ctor private void Trigger() { SetEventMask(EntityEvent.INIT /*| EntityEvent.TOUCH*/ | EntityEvent.FRAME | EntityEvent.ENTER | EntityEvent.LEAVE ); SetFlags(EntityFlags.TRIGGER, false); m_insiders = new array; } //! dtor private void ~Trigger() { #ifdef DIAG_DEVELOPER CleanupDebugShapes(dbgTargets); #endif } /** \name IEntity events Usage of IEntity events */ //@{ //! Set the default extents of the Trigger only once it is properly initialized override void EOnInit(IEntity other, int extra) { SetExtents("-2 -4 -2", "2 4 2"); } //! When an Object touches the Trigger, we want to register it being inside the Trigger -> Replaced by EOnEnter /*override void EOnTouch(IEntity other, int extra) { Object obj; if (Class.CastTo(obj, other) && CanAddObjectAsInsider(obj)) AddInsider(obj); }*/ //! We want to update the insiders every frame (or do we?) override void EOnFrame(IEntity other, float timeSlice) { UpdateInsiders(TIMEOUT); } //! When an Object enters the trigger add it to Insiders override void EOnEnter(IEntity other, int extra) { Object obj; if (Class.CastTo(obj, other) && CanAddObjectAsInsider(obj)) AddInsider(obj); } //! When an Object exits the trigger remove it from Insiders override void EOnLeave(IEntity other, int extra) { Object obj; if (Class.CastTo(obj, other)) RemoveInsiderByObject(obj); } //@} /** \name Trigger configuration Basic configuration and data API */ //@{ //! Set the size of the Trigger, avoid using SetCollisionBox directly void SetExtents(vector mins, vector maxs) { SetCollisionBox(mins, maxs); } //! Get the radius of the CollisionBox, simply left for backwards compatibility float GetRadius(vector min, vector max) { return GetCollisionRadius(); } //! Get the current TriggerInsider array, left for backwards compatibility, moved down from ManTrigger array GetInsiders() { return m_insiders; } //! Gets the TriggerInsider for the Object if it exists TriggerInsider GetInsiderForObject(Object object) { TriggerInsider ins; for ( int n = 0; n < m_insiders.Count(); ++n ) { ins = m_insiders[n]; if (ins.GetObject() == object) return ins; } return null; } //! Gets the index in m_insiders for the Object int GetInsiderIndexForObject(Object object) { TriggerInsider ins; for ( int n = 0; n < m_insiders.Count(); ++n ) { ins = m_insiders[n]; if (ins.GetObject() == object) return n; } return -1; } //@} /** \name TriggerEvents, backwards compatibility calling Implementation for backwards compatibility with old Trigger system */ //@{ override protected void OnEnterBeginEvent(TriggerInsider insider) { // Call the old event for backwards compatibility OnEnter(insider.GetObject()); } override protected void OnLeaveBeginEvent(TriggerInsider insider) { // Call the old event for backwards compatibility OnLeave(insider.GetObject()); } //@} /** \name DEPRECATED Events left for backwards compatibility */ //@{ void OnEnter(Object obj) {} void OnLeave(Object obj) {} //@} /** \name TriggerInsider conditions Conditions applied to TriggerInsider for if it can be added or should be removed */ //@{ //! Condition whether an Object can be added as TriggerInsider (checked before calling AddInsider) protected bool CanAddObjectAsInsider(Object object) { return true; } //! Condition whether a TriggerInsider should still be updated or not (checked in update loop and before adding) protected bool ShouldRemoveInsider(TriggerInsider insider) { return false; } //! Condition whether a TriggerInsider should still be updated or not, skips OnLeaveEvent (checked in update loop and before adding) protected bool ShouldRemoveInsiderNoLeave(TriggerInsider insider) { return false; } //@} /** \name TriggerInsider processing Logic and processing of adding, removing and updating a TriggerInsider */ //@{ //! Used for easily overriding TriggerInsider creation without rewriting AddInsider protected TriggerInsider CreateInsider(Object obj) { return new TriggerInsider(obj); } //! Adding of new TriggerInsider protected void AddInsider(Object obj) { if ( !obj ) return; // Already in? if ( GetInsiderForObject( obj ) ) { Error(string.Format("[WARNING] :: [Trigger] :: [%1] :: Insider (%2) is already inside.", GetDebugName(this), GetDebugName(obj))); return; } // New Object entered! Fill the data. TriggerInsider insider = CreateInsider(obj); insider.timeStamp = g_Game.GetTime(); insider.timeEntered = g_Game.GetTickTime(); insider.lastUpdated = insider.timeEntered; // Don't add if it is going to be removed anyways.. if ( ShouldRemoveInsider(insider) || ShouldRemoveInsiderNoLeave(insider) ) return; // Keep track of the Object as long as it is inside the Trigger int index = m_insiders.Insert(insider); // Call the enter event to signal this Object entered Enter(insider); obj.OnEnterTrigger(this); #ifdef TRIGGER_DEBUG_NORMAL Debug.TriggerLog(string.Format("%1: inserted at index %2", GetDebugName(obj), index), "Trigger", "", "AddInsider", GetDebugName(this)); #endif } //! Removing of TriggerInsider protected void RemoveInsider(TriggerInsider insider, int index = -1) { Leave(insider); insider.GetObject().OnLeaveTrigger(this); #ifdef TRIGGER_DEBUG_NORMAL Debug.TriggerLog(string.Format("%1: removing at index %2", GetDebugName(insider.GetObject()), index), "Trigger", "", "RemoveInsider", GetDebugName(this)); #endif if (index >= 0) m_insiders.Remove(index); else m_insiders.RemoveItemUnOrdered(insider); } //! Removing of TriggerInsider through Object protected void RemoveInsiderByObject(Object object) { TriggerInsider ins; for ( int n = 0; n < m_insiders.Count(); ++n ) { ins = m_insiders[n]; if (ins.GetObject() == object) { RemoveInsider(ins, n); return; } } // As EOnLeave can call this, it is perfectly valid that this Object is not found on Script side // because of "ShouldRemoveInsider" and "ShouldRemoveInsiderNoLeave" } //! Update the current TriggerInsider inside the Trigger, timeout paramter is deprecated protected void UpdateInsiders(int timeout) { #ifdef DIAG_DEVELOPER DebugSendDmgTrigger(); #endif // Don't do anything if there aren't any insiders if ( m_insiders.Count() == 0 ) return; // Mark the beginning of the update loop StayStart(m_insiders.Count()); // Iterate over the current insiders, backwards because we are deleting for ( int n = m_insiders.Count() - 1; n >= 0 ; --n) { TriggerInsider insider = m_insiders.Get(n); Object obj = insider.GetObject(); // Check if the Object still exists or should be removed without calling OnLeaveEvent if ( !obj || ShouldRemoveInsiderNoLeave(insider) ) { #ifdef TRIGGER_DEBUG_BASIC Debug.TriggerLog(string.Format("%1: removed with no Leave.", GetDebugName(obj)), "Trigger", "", "UpdateInsiders", GetDebugName(this)); #endif m_insiders.Remove(n); continue; } // Check if Object left the Trigger or should be removed regardless if ( ShouldRemoveInsider(insider) ) { RemoveInsider(insider, n); continue; } // Call the OnStayEvent, Object is still inside the Trigger and can be updated // Pass in the time since the Object was last updated (or entered) float currentTime = g_Game.GetTickTime(); Stay(insider, currentTime - insider.lastUpdated); insider.lastUpdated = currentTime; } // Mark the end of the update loop StayFinish(); } //@} /** \name DEBUGGING General internal debugging functionality */ //@{ override void OnRPC(PlayerIdentity sender, int rpc_type, ParamsReadContext ctx) { super.OnRPC(sender, rpc_type, ctx); #ifdef DIAG_DEVELOPER switch (rpc_type) { case ERPCs.DIAG_TRIGGER_DEBUG: DebugTriggerInfo data = new DebugTriggerInfo(vector.Zero, vector.Zero, vector.Zero, vector.Zero, 0, "", null); if (ctx.Read(data)) DebugDmgTrigger(data.param1, data.param2, data.param3, data.param4, data.param5, data.param6, data.param7); break; } #endif } #ifdef DIAG_DEVELOPER void DebugSendDmgTrigger() { vector minmax[2]; GetCollisionBox(minmax); DebugTriggerInfo data = new DebugTriggerInfo(vector.Zero, vector.Zero, vector.Zero, vector.Zero, 0, "", null); data.param1 = GetWorldPosition(); data.param2 = GetOrientation(); data.param3 = minmax[0]; data.param4 = minmax[1]; data.param5 = GetCollisionRadius(); data.param6 = m_DebugAreaType; data.param7 = m_insiders; if (GetGame().IsMultiplayer() && GetGame().IsServer()) PluginDiagMenuServer.SendDataToSubscribersServer(this, ESubscriberSystems.TRIGGERS, ERPCs.DIAG_TRIGGER_DEBUG, data, false); else if (!GetGame().IsMultiplayer() || m_Local) DebugDmgTrigger(data.param1, data.param2, data.param3, data.param4, data.param5, data.param6, data.param7); } protected ref array dbgTargets = new array(); void DebugDmgTrigger( vector pos, vector orientation, vector min, vector max, float radius, string dmgType, array insiders) { CleanupDebugShapes(dbgTargets); bool enableDebug = DiagMenu.GetBool(DiagMenuIDs.TRIGGER_DEBUG); if (enableDebug) { if (GetGame().IsMultiplayer() && GetGame().IsServer()) { return; } vector w_pos, w_pos_sphr, w_pos_lend; w_pos = pos; // sphere pos tweaks w_pos_sphr = w_pos; // line pos tweaks w_pos_lend = w_pos; //Find way to change colour of box depending on ammoType in a more elegant fashion m_DebugAreaType = dmgType; Shape dbgShape; switch ( m_DebugAreaType ) { case "FireDamage": dbgShape = DrawDebugShape(pos, min, max, radius, COLOR_RED_A); break; case "BarbedWireHit": dbgShape = DrawDebugShape(pos, min, max, radius, COLOR_BLUE_A); break; default: dbgShape = DrawDebugShape(pos, min, max, radius, COLOR_GREEN_A); break; } if (GetGame().IsMultiplayer() || GetGame().IsServer()) m_dbgInsiders = insiders; if (m_dbgInsiders.Count() > 0) { //Change colour to make state clearer dbgShape.SetColor(COLOR_YELLOW_A); for (int i = 0; i < m_dbgInsiders.Count(); i++) { EntityAI insider_EAI = EntityAI.Cast(m_dbgInsiders[i].GetObject()); if (insider_EAI) { vector insiderPos = insider_EAI.GetWorldPosition() + "0 0.1 0"; dbgTargets.Insert(Debug.DrawArrow(w_pos, insiderPos)); } } } } } protected Shape DrawDebugShape(vector pos, vector min, vector max, float radius, int color) { Shape dbgShape; switch (GetTriggerShape()) { case TriggerShape.BOX: dbgShape = Debug.DrawBoxEx(min, max, color, ShapeFlags.TRANSP|ShapeFlags.NOZWRITE); vector mat[4]; GetTransform(mat); dbgShape.CreateMatrix(mat); dbgShape.SetMatrix(mat); break; case TriggerShape.CYLINDER: dbgShape = Debug.DrawCylinder(pos, radius, max[1], color, ShapeFlags.TRANSP|ShapeFlags.NOZWRITE); break; case TriggerShape.SPHERE: dbgShape = Debug.DrawSphere(pos, radius, color, ShapeFlags.TRANSP|ShapeFlags.NOZWRITE); break; default: ErrorEx("TriggerShape not found", ErrorExSeverity.WARNING); break; } dbgTargets.Insert(dbgShape); return dbgShape; } protected void CleanupDebugShapes(array shapes) { foreach (Shape shape : shapes) { Debug.RemoveShape(shape); } shapes.Clear(); } #endif //@} };