(61d00a474) v0.9.7.1
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
/*
|
||||
// Copyright (c) 2017 Kastellanos Nikolaos
|
||||
|
||||
/* Original source Farseer Physics Engine:
|
||||
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
|
||||
* Microsoft Permissive License (Ms-PL) v1.1
|
||||
*/
|
||||
|
||||
/*
|
||||
* Farseer Physics Engine:
|
||||
* Copyright (c) 2012 Ian Qvist
|
||||
*
|
||||
@@ -19,7 +26,6 @@
|
||||
* misrepresented as being the original software.
|
||||
* 3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
//#define USE_ACTIVE_CONTACT_SET
|
||||
|
||||
using System.Collections.Generic;
|
||||
using FarseerPhysics.Collision;
|
||||
@@ -29,6 +35,33 @@ namespace FarseerPhysics.Dynamics
|
||||
{
|
||||
public class ContactManager
|
||||
{
|
||||
#region Settings
|
||||
/// <summary>
|
||||
/// A threshold for activating multiple cores to solve VelocityConstraints.
|
||||
/// An Island with a contact count above this threshold will use multiple threads to solve VelocityConstraints.
|
||||
/// A value of 0 will always use multithreading. A value of (int.MaxValue) will never use multithreading.
|
||||
/// Typical values are {128 or 256}.
|
||||
/// </summary>
|
||||
public int VelocityConstraintsMultithreadThreshold = 128;
|
||||
|
||||
/// <summary>
|
||||
/// A threshold for activating multiple cores to solve PositionConstraints.
|
||||
/// An Island with a contact count above this threshold will use multiple threads to solve PositionConstraints.
|
||||
/// A value of 0 will always use multithreading. A value of (int.MaxValue) will never use multithreading.
|
||||
/// Typical values are {128 or 256}.
|
||||
/// </summary>
|
||||
public int PositionConstraintsMultithreadThreshold = 128;
|
||||
|
||||
/// <summary>
|
||||
/// A threshold for activating multiple cores to solve Collide.
|
||||
/// An World with a contact count above this threshold will use multiple threads to solve Collide.
|
||||
/// A value of 0 will always use multithreading. A value of (int.MaxValue) will never use multithreading.
|
||||
/// Typical values are {128 or 256}.
|
||||
/// </summary>
|
||||
public int CollideMultithreadThreshold = 64;
|
||||
#endregion
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fires when a contact is created
|
||||
/// </summary>
|
||||
@@ -36,12 +69,15 @@ namespace FarseerPhysics.Dynamics
|
||||
|
||||
public IBroadPhase BroadPhase;
|
||||
|
||||
public readonly ContactListHead ContactList;
|
||||
public int ContactCount { get; private set; }
|
||||
internal readonly ContactListHead _contactPoolList;
|
||||
|
||||
/// <summary>
|
||||
/// The filter used by the contact manager.
|
||||
/// </summary>
|
||||
public CollisionFilterDelegate ContactFilter;
|
||||
|
||||
public List<Contact> ContactList = new List<Contact>(256);
|
||||
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
/// <summary>
|
||||
@@ -79,21 +115,22 @@ namespace FarseerPhysics.Dynamics
|
||||
|
||||
internal ContactManager(IBroadPhase broadPhase)
|
||||
{
|
||||
ContactList = new ContactListHead();
|
||||
ContactCount = 0;
|
||||
_contactPoolList = new ContactListHead();
|
||||
|
||||
BroadPhase = broadPhase;
|
||||
OnBroadphaseCollision = AddPair;
|
||||
}
|
||||
|
||||
// Broad-phase callback.
|
||||
private void AddPair(ref FixtureProxy proxyA, ref FixtureProxy proxyB)
|
||||
private void AddPair(int proxyIdA, int proxyIdB)
|
||||
{
|
||||
Fixture fixtureA = proxyA.Fixture;
|
||||
Fixture fixtureB = proxyB.Fixture;
|
||||
FixtureProxy proxyA = BroadPhase.GetProxy(proxyIdA);
|
||||
FixtureProxy proxyB = BroadPhase.GetProxy(proxyIdB);
|
||||
|
||||
int indexA = proxyA.ChildIndex;
|
||||
int indexB = proxyB.ChildIndex;
|
||||
|
||||
Body bodyA = fixtureA.Body;
|
||||
Body bodyB = fixtureB.Body;
|
||||
Body bodyA = proxyA.Body;
|
||||
Body bodyB = proxyB.Body;
|
||||
|
||||
// Are the fixtures on the same body?
|
||||
if (bodyA == bodyB)
|
||||
@@ -101,16 +138,21 @@ namespace FarseerPhysics.Dynamics
|
||||
return;
|
||||
}
|
||||
|
||||
Fixture fixtureA = proxyA.Fixture;
|
||||
Fixture fixtureB = proxyB.Fixture;
|
||||
|
||||
int indexA = proxyA.ChildIndex;
|
||||
int indexB = proxyB.ChildIndex;
|
||||
|
||||
// Does a contact already exist?
|
||||
ContactEdge edge = bodyB.ContactList;
|
||||
while (edge != null)
|
||||
for (ContactEdge ceB = bodyB.ContactList; ceB != null; ceB = ceB.Next)
|
||||
{
|
||||
if (edge.Other == bodyA)
|
||||
if (ceB.Other == bodyA)
|
||||
{
|
||||
Fixture fA = edge.Contact.FixtureA;
|
||||
Fixture fB = edge.Contact.FixtureB;
|
||||
int iA = edge.Contact.ChildIndexA;
|
||||
int iB = edge.Contact.ChildIndexB;
|
||||
Fixture fA = ceB.Contact.FixtureA;
|
||||
Fixture fB = ceB.Contact.FixtureB;
|
||||
int iA = ceB.Contact.ChildIndexA;
|
||||
int iB = ceB.Contact.ChildIndexB;
|
||||
|
||||
if (fA == fixtureA && fB == fixtureB && iA == indexA && iB == indexB)
|
||||
{
|
||||
@@ -124,8 +166,6 @@ namespace FarseerPhysics.Dynamics
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
edge = edge.Next;
|
||||
}
|
||||
|
||||
// Does a joint override collision? Is at least one body dynamic?
|
||||
@@ -148,7 +188,7 @@ namespace FarseerPhysics.Dynamics
|
||||
return;
|
||||
|
||||
// Call the factory.
|
||||
Contact c = Contact.Create(fixtureA, indexA, fixtureB, indexB);
|
||||
Contact c = Contact.Create(this, fixtureA, indexA, fixtureB, indexB);
|
||||
|
||||
if (c == null)
|
||||
return;
|
||||
@@ -160,7 +200,11 @@ namespace FarseerPhysics.Dynamics
|
||||
bodyB = fixtureB.Body;
|
||||
|
||||
// Insert into the world.
|
||||
ContactList.Add(c);
|
||||
c.Prev = ContactList;
|
||||
c.Next = c.Prev.Next;
|
||||
c.Prev.Next = c;
|
||||
c.Next.Prev = c;
|
||||
ContactCount++;
|
||||
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
ActiveContacts.Add(c);
|
||||
@@ -215,73 +259,79 @@ namespace FarseerPhysics.Dynamics
|
||||
{
|
||||
//Report the separation to both participants:
|
||||
if (fixtureA != null && fixtureA.OnSeparation != null)
|
||||
fixtureA.OnSeparation(fixtureA, fixtureB);
|
||||
fixtureA.OnSeparation(fixtureA, fixtureB, contact);
|
||||
|
||||
//Reverse the order of the reported fixtures. The first fixture is always the one that the
|
||||
//user subscribed to.
|
||||
if (fixtureB != null && fixtureB.OnSeparation != null)
|
||||
fixtureB.OnSeparation(fixtureB, fixtureA);
|
||||
fixtureB.OnSeparation(fixtureB, fixtureA, contact);
|
||||
|
||||
//Report the separation to both bodies:
|
||||
if (fixtureA != null && fixtureA.Body != null && fixtureA.Body.onSeparationEventHandler != null)
|
||||
fixtureA.Body.onSeparationEventHandler(fixtureA, fixtureB, contact);
|
||||
|
||||
//Reverse the order of the reported fixtures. The first fixture is always the one that the
|
||||
//user subscribed to.
|
||||
if (fixtureB != null && fixtureB.Body != null && fixtureB.Body.onSeparationEventHandler != null)
|
||||
fixtureB.Body.onSeparationEventHandler(fixtureB, fixtureA, contact);
|
||||
|
||||
if (EndContact != null)
|
||||
EndContact(contact);
|
||||
}
|
||||
|
||||
// Remove from the world.
|
||||
ContactList.Remove(contact);
|
||||
contact.Prev.Next = contact.Next;
|
||||
contact.Next.Prev = contact.Prev;
|
||||
contact.Next = null;
|
||||
contact.Prev = null;
|
||||
ContactCount--;
|
||||
|
||||
// Remove from body 1
|
||||
if (contact._nodeA.Prev != null)
|
||||
{
|
||||
contact._nodeA.Prev.Next = contact._nodeA.Next;
|
||||
}
|
||||
|
||||
if (contact._nodeA.Next != null)
|
||||
{
|
||||
contact._nodeA.Next.Prev = contact._nodeA.Prev;
|
||||
}
|
||||
|
||||
if (contact._nodeA == bodyA.ContactList)
|
||||
{
|
||||
bodyA.ContactList = contact._nodeA.Next;
|
||||
}
|
||||
if (contact._nodeA.Prev != null)
|
||||
contact._nodeA.Prev.Next = contact._nodeA.Next;
|
||||
if (contact._nodeA.Next != null)
|
||||
contact._nodeA.Next.Prev = contact._nodeA.Prev;
|
||||
|
||||
// Remove from body 2
|
||||
if (contact._nodeB.Prev != null)
|
||||
{
|
||||
contact._nodeB.Prev.Next = contact._nodeB.Next;
|
||||
}
|
||||
|
||||
if (contact._nodeB.Next != null)
|
||||
{
|
||||
contact._nodeB.Next.Prev = contact._nodeB.Prev;
|
||||
}
|
||||
|
||||
if (contact._nodeB == bodyB.ContactList)
|
||||
{
|
||||
bodyB.ContactList = contact._nodeB.Next;
|
||||
}
|
||||
if (contact._nodeB.Prev != null)
|
||||
contact._nodeB.Prev.Next = contact._nodeB.Next;
|
||||
if (contact._nodeB.Next != null)
|
||||
contact._nodeB.Next.Prev = contact._nodeB.Prev;
|
||||
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
if (ActiveContacts.Contains(contact))
|
||||
{
|
||||
ActiveContacts.Remove(contact);
|
||||
}
|
||||
#endif
|
||||
contact.Destroy();
|
||||
|
||||
// Insert into the pool.
|
||||
contact.Next = _contactPoolList.Next;
|
||||
_contactPoolList.Next = contact;
|
||||
}
|
||||
|
||||
internal void Collide()
|
||||
{
|
||||
#if NET40 || NET45 || PORTABLE40 || PORTABLE45 || W10 || W8_1 || WP8_1
|
||||
if (this.ContactCount > CollideMultithreadThreshold && System.Environment.ProcessorCount > 1)
|
||||
{
|
||||
CollideMultiCore();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Update awake contacts.
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
ActiveList.AddRange(ActiveContacts);
|
||||
|
||||
foreach (var c in ActiveList)
|
||||
{
|
||||
#else
|
||||
for (int i = 0; i < ContactList.Count; i++)
|
||||
ActiveList.AddRange(ActiveContacts);
|
||||
foreach (var tmpc in ActiveList)
|
||||
{
|
||||
Contact c = tmpc;
|
||||
#else
|
||||
for (Contact c = ContactList.Next; c != ContactList;)
|
||||
{
|
||||
Contact c = ContactList[i];
|
||||
#endif
|
||||
Fixture fixtureA = c.FixtureA;
|
||||
Fixture fixtureB = c.FixtureB;
|
||||
@@ -292,7 +342,10 @@ namespace FarseerPhysics.Dynamics
|
||||
|
||||
//Do no try to collide disabled bodies
|
||||
if (!bodyA.Enabled || !bodyB.Enabled)
|
||||
{
|
||||
c = c.Next;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is this contact flagged for filtering?
|
||||
if (c.FilterFlag)
|
||||
@@ -301,6 +354,7 @@ namespace FarseerPhysics.Dynamics
|
||||
if (bodyB.ShouldCollide(bodyA) == false)
|
||||
{
|
||||
Contact cNuke = c;
|
||||
c = c.Next;
|
||||
Destroy(cNuke);
|
||||
continue;
|
||||
}
|
||||
@@ -309,6 +363,7 @@ namespace FarseerPhysics.Dynamics
|
||||
if (ShouldCollide(fixtureA, fixtureB) == false)
|
||||
{
|
||||
Contact cNuke = c;
|
||||
c = c.Next;
|
||||
Destroy(cNuke);
|
||||
continue;
|
||||
}
|
||||
@@ -317,6 +372,7 @@ namespace FarseerPhysics.Dynamics
|
||||
if (ContactFilter != null && ContactFilter(fixtureA, fixtureB) == false)
|
||||
{
|
||||
Contact cNuke = c;
|
||||
c = c.Next;
|
||||
Destroy(cNuke);
|
||||
continue;
|
||||
}
|
||||
@@ -334,6 +390,7 @@ namespace FarseerPhysics.Dynamics
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
ActiveContacts.Remove(c);
|
||||
#endif
|
||||
c = c.Next;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -346,12 +403,15 @@ namespace FarseerPhysics.Dynamics
|
||||
if (overlap == false)
|
||||
{
|
||||
Contact cNuke = c;
|
||||
c = c.Next;
|
||||
Destroy(cNuke);
|
||||
continue;
|
||||
}
|
||||
|
||||
// The contact persists.
|
||||
c.Update(this);
|
||||
|
||||
c = c.Next;
|
||||
}
|
||||
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
@@ -359,87 +419,196 @@ namespace FarseerPhysics.Dynamics
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A temporary list of contacts to be updated during Collide().
|
||||
/// </summary>
|
||||
List<Contact> updateList = new List<Contact>();
|
||||
|
||||
#if NET40 || NET45 || PORTABLE40 || PORTABLE45 || W10 || W8_1 || WP8_1
|
||||
internal void CollideMultiCore()
|
||||
{
|
||||
int lockOrder = 0;
|
||||
|
||||
// Update awake contacts.
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
ActiveList.AddRange(ActiveContacts);
|
||||
foreach (var tmpc in ActiveList)
|
||||
{
|
||||
Contact c = tmpc;
|
||||
#else
|
||||
for (Contact c = ContactList.Next; c != ContactList; )
|
||||
{
|
||||
#endif
|
||||
Fixture fixtureA = c.FixtureA;
|
||||
Fixture fixtureB = c.FixtureB;
|
||||
int indexA = c.ChildIndexA;
|
||||
int indexB = c.ChildIndexB;
|
||||
Body bodyA = fixtureA.Body;
|
||||
Body bodyB = fixtureB.Body;
|
||||
|
||||
//Do no try to collide disabled bodies
|
||||
if (!bodyA.Enabled || !bodyB.Enabled)
|
||||
{
|
||||
c = c.Next;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is this contact flagged for filtering?
|
||||
if (c.FilterFlag)
|
||||
{
|
||||
// Should these bodies collide?
|
||||
if (bodyB.ShouldCollide(bodyA) == false)
|
||||
{
|
||||
Contact cNuke = c;
|
||||
c = c.Next;
|
||||
Destroy(cNuke);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check default filtering
|
||||
if (ShouldCollide(fixtureA, fixtureB) == false)
|
||||
{
|
||||
Contact cNuke = c;
|
||||
c = c.Next;
|
||||
Destroy(cNuke);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check user filtering.
|
||||
if (ContactFilter != null && ContactFilter(fixtureA, fixtureB) == false)
|
||||
{
|
||||
Contact cNuke = c;
|
||||
c = c.Next;
|
||||
Destroy(cNuke);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clear the filtering flag.
|
||||
c.FilterFlag = false;
|
||||
}
|
||||
|
||||
bool activeA = bodyA.Awake && bodyA.BodyType != BodyType.Static;
|
||||
bool activeB = bodyB.Awake && bodyB.BodyType != BodyType.Static;
|
||||
|
||||
// At least one body must be awake and it must be dynamic or kinematic.
|
||||
if (activeA == false && activeB == false)
|
||||
{
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
ActiveContacts.Remove(c);
|
||||
#endif
|
||||
c = c.Next;
|
||||
continue;
|
||||
}
|
||||
|
||||
int proxyIdA = fixtureA.Proxies[indexA].ProxyId;
|
||||
int proxyIdB = fixtureB.Proxies[indexB].ProxyId;
|
||||
|
||||
bool overlap = BroadPhase.TestOverlap(proxyIdA, proxyIdB);
|
||||
|
||||
// Here we destroy contacts that cease to overlap in the broad-phase.
|
||||
if (overlap == false)
|
||||
{
|
||||
Contact cNuke = c;
|
||||
c = c.Next;
|
||||
Destroy(cNuke);
|
||||
continue;
|
||||
}
|
||||
|
||||
// The contact persists.
|
||||
updateList.Add(c);
|
||||
// Assign a unique id for lock order
|
||||
bodyA._lockOrder = lockOrder++;
|
||||
bodyB._lockOrder = lockOrder++;
|
||||
|
||||
|
||||
c = c.Next;
|
||||
}
|
||||
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
ActiveList.Clear();
|
||||
#endif
|
||||
|
||||
// update contacts
|
||||
System.Threading.Tasks.Parallel.ForEach<Contact>(updateList, (c) =>
|
||||
{
|
||||
// find lower order item
|
||||
Fixture fixtureA = c.FixtureA;
|
||||
Fixture fixtureB = c.FixtureB;
|
||||
|
||||
// find lower order item
|
||||
Body orderedBodyA = fixtureA.Body;
|
||||
Body orderedBodyB = fixtureB.Body;
|
||||
int idA = orderedBodyA._lockOrder;
|
||||
int idB = orderedBodyB._lockOrder;
|
||||
if (idA == idB)
|
||||
throw new System.Exception();
|
||||
|
||||
if (idA > idB)
|
||||
{
|
||||
orderedBodyA = fixtureB.Body;
|
||||
orderedBodyB = fixtureA.Body;
|
||||
}
|
||||
|
||||
// obtain lock
|
||||
for (; ; )
|
||||
{
|
||||
if (System.Threading.Interlocked.CompareExchange(ref orderedBodyA._lock, 1, 0) == 0)
|
||||
{
|
||||
if (System.Threading.Interlocked.CompareExchange(ref orderedBodyB._lock, 1, 0) == 0)
|
||||
break;
|
||||
System.Threading.Interlocked.Exchange(ref orderedBodyA._lock, 0);
|
||||
}
|
||||
#if NET40 || NET45
|
||||
System.Threading.Thread.Sleep(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
c.Update(this);
|
||||
|
||||
System.Threading.Interlocked.Exchange(ref orderedBodyB._lock, 0);
|
||||
System.Threading.Interlocked.Exchange(ref orderedBodyA._lock, 0);
|
||||
});
|
||||
|
||||
updateList.Clear();
|
||||
}
|
||||
#endif
|
||||
|
||||
private static bool ShouldCollide(Fixture fixtureA, Fixture fixtureB)
|
||||
{
|
||||
if (Settings.UseFPECollisionCategories)
|
||||
if (fixtureA.CollisionGroup != 0 && fixtureA.CollisionGroup == fixtureB.CollisionGroup)
|
||||
{
|
||||
if ((fixtureA.CollisionGroup == fixtureB.CollisionGroup) &&
|
||||
fixtureA.CollisionGroup != 0 && fixtureB.CollisionGroup != 0)
|
||||
return false;
|
||||
|
||||
if (((fixtureA.CollisionCategories & fixtureB.CollidesWith) ==
|
||||
Category.None) &
|
||||
((fixtureB.CollisionCategories & fixtureA.CollidesWith) ==
|
||||
Category.None))
|
||||
return false;
|
||||
|
||||
if (fixtureA.IsFixtureIgnored(fixtureB) ||
|
||||
fixtureB.IsFixtureIgnored(fixtureA))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
return (fixtureA.CollisionGroup > 0);
|
||||
}
|
||||
|
||||
if (fixtureA.CollisionGroup == fixtureB.CollisionGroup &&
|
||||
fixtureA.CollisionGroup != 0)
|
||||
{
|
||||
return fixtureA.CollisionGroup > 0;
|
||||
}
|
||||
|
||||
bool collide = (fixtureA.CollidesWith & fixtureB.CollisionCategories) != 0 &&
|
||||
(fixtureA.CollisionCategories & fixtureB.CollidesWith) != 0;
|
||||
|
||||
if (collide)
|
||||
{
|
||||
if (fixtureA.IsFixtureIgnored(fixtureB) ||
|
||||
fixtureB.IsFixtureIgnored(fixtureA))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool collide = ((fixtureA.CollidesWith & fixtureB.CollisionCategories) != 0) &&
|
||||
((fixtureB.CollidesWith & fixtureA.CollisionCategories) != 0);
|
||||
|
||||
return collide;
|
||||
}
|
||||
|
||||
internal void UpdateContacts(ContactEdge contactEdge, bool value)
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
internal void UpdateActiveContacts(ContactEdge ContactList, bool value)
|
||||
{
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
if(value)
|
||||
{
|
||||
while(contactEdge != null)
|
||||
{
|
||||
var c = contactEdge.Contact;
|
||||
if (!ActiveContacts.Contains(c))
|
||||
{
|
||||
ActiveContacts.Add(c);
|
||||
}
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (contactEdge != null)
|
||||
{
|
||||
var c = contactEdge.Contact;
|
||||
if (!contactEdge.Other.Awake)
|
||||
{
|
||||
if (ActiveContacts.Contains(c))
|
||||
{
|
||||
ActiveContacts.Remove(c);
|
||||
}
|
||||
}
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if(value)
|
||||
{
|
||||
for (var contactEdge = ContactList; contactEdge != null; contactEdge = contactEdge.Next)
|
||||
{
|
||||
if (!ActiveContacts.Contains(contactEdge.Contact))
|
||||
ActiveContacts.Add(contactEdge.Contact);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var contactEdge = ContactList; contactEdge != null; contactEdge = contactEdge.Next)
|
||||
{
|
||||
if (!contactEdge.Other.Awake)
|
||||
{
|
||||
if (ActiveContacts.Contains(contactEdge.Contact))
|
||||
ActiveContacts.Remove(contactEdge.Contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if USE_ACTIVE_CONTACT_SET
|
||||
internal void RemoveActiveContact(Contact contact)
|
||||
{
|
||||
if (ActiveContacts.Contains(contact))
|
||||
ActiveContacts.Remove(contact);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user