diff --git a/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala b/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala
index 406b09a4..0a9489f9 100644
--- a/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala
+++ b/common/src/main/scala/net/psforever/objects/entity/IdentifiableEntity.scala
@@ -1,104 +1,110 @@
-// Copyright (c) 2017 PSForever
+// Copyright (c) 2017-2019 PSForever
package net.psforever.objects.entity
-import net.psforever.types.PlanetSideGUID
+import net.psforever.types.{PlanetSideGUID, StalePlanetSideGUID}
/**
- * Represent any entity that must have its own globally unique identifier (GUID) to be functional.
+ * Represent any entity that must have its own valid global unique identifier (GUID) to be functional.
*
- * "Testing" the object refers to the act of acquiring a reference to the GUID the object is using.
- * This object starts with a container class that represents a unprepared GUID state and raises an `Exception` when tested.
- * Setting a proper `PlanetSideGUID` replaces that container class with a container class that returns the GUID when tested.
- * The object can be invalidated, retaining the previous identifier number, but marking that object as being "stale."
- * "Staleness" is a property indicating whether or not the number can be used as a valid representation of the object.
+ * The basic design philosophy of the workflow of a GUID at this stage is a deterministic state machine.
+ * At the start, an `Exception` will be thrown while the default conditions of the accessor and mutator are maintained.
+ * ("The ability to set a new valid GUID".)
+ * Only a valid GUID may be set and, once it does, that changes the conditions of the accessor and mutator
+ * to one where it will return the valid GUID and one where it will no longer accept a new GUID (valid or invalid).
+ * That GUID will continue being the GUID reported by the object, even if another valid GUID tries to be set.
+ * (No error or exception will be thrown.)
+ * To set a new GUID, the current one must be invalidated with the appropriate function,
+ * and this turns both the object and any object reference that can be acquired from the object "stale."
+ * Doing this prior to setting the initial valid GUID is fruitless
+ * as it restores the object to its default mutation option ("the ability to set a new valid GUID").
+ * Access to the GUID is retained.
+ * This can be done as many times as is necessary by following the same order of actions.
+ *
+ * The "staleness" of the object and the "staleness" of the GUID are related.
+ * The condition in general indicates that the object has somehow become externally disconnected from its GUID reference
+ * though the two still share something similar to their prior relationship internally.
+ * Do not expect a "stale" GUID to refer to the same object through some mapping mechanism.
+ * Do not expect a "stale" object to give you a GUID that will map back to itself.
* @throws `NoGUIDException` if a GUID has not yet been assigned
*/
abstract class IdentifiableEntity extends Identifiable {
+ /** storage for the GUID information; starts as a `StalePlanetSideGUID` */
+ private var current : PlanetSideGUID = StalePlanetSideGUID(0)
/** indicate the validity of the current GUID */
- private var stale : Boolean = true
- /** storage for the active GUID */
- private val container : GUIDContainable = GUIDContainer()
- /** the handle for the active GUID; starts as exception-throwing */
- private var current : GUIDContainable = IdentifiableEntity.noGUIDContainer
+ private var guidValid : Boolean = false
+ /** the current accessor; can be re-assigned */
+ private var guidAccessor : IdentifiableEntity=>PlanetSideGUID = IdentifiableEntity.noGuidGet
+ /** the current mutator; can be re-assigned */
+ private var guidMutator : (IdentifiableEntity, PlanetSideGUID)=>PlanetSideGUID = IdentifiableEntity.noGuidSet
+
+ def GUID : PlanetSideGUID = guidAccessor(this)
+
+ /** Always intercept `StalePlanetSideGUID` references when attempting to mutate the GUID value. */
+ def GUID_=(guid : StalePlanetSideGUID) : PlanetSideGUID = guidAccessor(this)
+
+ def GUID_=(guid : PlanetSideGUID) : PlanetSideGUID = guidMutator(this, guid)
/**
- * The object will not originally having a valid GUID,
- * so "stale" will be used to expressed "not initialized."
- * After being set and then properly invalidated, then it will indicate proper staleness.
+ * Flag when the object has no GUID (initial condition) or is considered stale.
* @return whether the value of the GUID is a valid representation for this object
*/
- def HasGUID : Boolean = !stale
-
- def GUID : PlanetSideGUID = current.GUID
-
- def GUID_=(guid : PlanetSideGUID) : PlanetSideGUID = {
- stale = false
- current = container
- current.GUID = guid
- GUID
- }
+ def HasGUID : Boolean = guidValid
/**
- * Set the staleness to indicate whether the GUID has ever been set
- * or that the set GUID is not a proper representation of the object.
- * It is always set to `true`.
+ * Indicate that the current GUID is no longer a valid representation of the object.
+ * Transforms whatever the current GUID is into a `StalePlanetSideGUID` entity with the same value.
+ * Doing this restores the object to its default mutation option ("the ability to set a new valid GUID").
+ * The current GUID will still be accessed as if it were valid, but it will be wrapped in the new stale object.
*/
def Invalidate() : Unit = {
- stale = true
+ guidValid = false
+ current = StalePlanetSideGUID(current.guid)
+ guidMutator = IdentifiableEntity.noGuidSet
}
}
object IdentifiableEntity {
- private val noGUIDContainer : GUIDContainable = new NoGUIDContainer
-}
-
-/**
- * Mask the `Identifiable` `trait`.
- */
-sealed trait GUIDContainable extends Identifiable
-
-/**
- * Hidden container that represents an object that is not ready to be used by the game.
- */
-private case class NoGUIDContainer() extends GUIDContainable {
/**
- * Raise an `Exception` because we have no GUID to give.
+ * Raise an `Exception` because the entity is never considered having a GUID to give.
+ * @param o the any entity with a GUID
* @throws `NoGUIDException` always
* @return never returns
*/
- def GUID : PlanetSideGUID = {
- throw NoGUIDException(s"object $this has not initialized a global identifier")
+ def noGuidGet(o : IdentifiableEntity) : PlanetSideGUID = {
+ throw NoGUIDException(s"did not initialize this object $o with a valid global identifier")
}
/**
- * Normally, this should never be called.
- * @param toGuid the globally unique identifier
- * @return never returns
- */
- def GUID_=(toGuid : PlanetSideGUID) : PlanetSideGUID = {
- throw NoGUIDException("can not initialize a global identifier with this object")
- }
-}
-
-/**
- * Hidden container that represents an object that has a working GUID and is ready to be used by the game.
- * @param guid the object's globally unique identifier;
- * defaults to a GUID equal to 0
- */
-private case class GUIDContainer(private var guid : PlanetSideGUID = PlanetSideGUID(0)) extends GUIDContainable {
- /**
- * Provide the GUID used to initialize this object.
+ * Provide the entity with a valid GUID replacing an invalid GUID.
+ * Modify the accessor and mutator function literals to ensure the entity will remain stable.
+ * It will not be mutated by a new valid value without the existing valid value having to first be invalidated.
+ * Its access is made standard.
+ * @param o the any entity with a GUID
+ * @param guid the valid GUID to assign
* @return the GUID
*/
- def GUID : PlanetSideGUID = guid
+ def noGuidSet(o : IdentifiableEntity, guid : PlanetSideGUID) : PlanetSideGUID = {
+ o.current = guid
+ o.guidValid = true
+ o.guidAccessor = guidGet
+ o.guidMutator = guidSet
+ guid
+ }
/**
- * Exchange the previous GUID for a new one, re-using this container.
- * @param toGuid the globally unique identifier
- * @return the GUID
+ * The entity should have a valid GUID that can be provided.
+ * @param o the entity
+ * @return the entity's GUID
*/
- def GUID_=(toGuid : PlanetSideGUID) : PlanetSideGUID = {
- guid = toGuid
- GUID
- }
+ def guidGet(o : IdentifiableEntity) : PlanetSideGUID = o.current
+
+ /**
+ * The entity is in a condition where it can not be assigned the new valid GUID.
+ * This state establishes itself after setting the very first valid GUID and
+ * will persist to the end of the entity's life.
+ * @param o the any entity with a GUID
+ * @param guid the valid GUID to assign
+ * @return the entity's GUID
+ */
+ def guidSet(o : IdentifiableEntity, guid : PlanetSideGUID) : PlanetSideGUID = o.current
}
diff --git a/common/src/main/scala/net/psforever/types/PlanetSideGUID.scala b/common/src/main/scala/net/psforever/types/PlanetSideGUID.scala
index 30aac5db..347267be 100644
--- a/common/src/main/scala/net/psforever/types/PlanetSideGUID.scala
+++ b/common/src/main/scala/net/psforever/types/PlanetSideGUID.scala
@@ -1,9 +1,40 @@
+// Copyright (c) 2017-2019 PSForever
package net.psforever.types
import scodec.codecs.uint16L
-case class PlanetSideGUID(guid : Int)
+abstract class PlanetSideGUID {
+ def guid : Int
+
+ /* overriding equals and hashCode to benefit the case class subclasses through inheritance;
+ * essentially, if not for these overrides, each case class would implement its own equivalence methods
+ * */
+ /**
+ * All subclasses of `PlanetSideGUID` are equivalent through being subclasses of `PlanetSideGUID`.
+ * @param o an entity
+ * @return whether that entity is a `PlanetSideGUID` object
+ */
+ def canEqual(o : Any) : Boolean = o.isInstanceOf[PlanetSideGUID]
+
+ override def equals(o : Any) : Boolean = o match {
+ case that : PlanetSideGUID => that.canEqual(this) && that.guid == this.guid
+ case _ => false
+ }
+
+ override def hashCode: Int = java.util.Objects.hashCode(guid)
+}
+
+final case class ValidPlanetSideGUID(guid : Int) extends PlanetSideGUID
+
+final case class StalePlanetSideGUID(guid : Int) extends PlanetSideGUID
object PlanetSideGUID {
- implicit val codec = uint16L.as[PlanetSideGUID]
+ def apply(guid : Int) : PlanetSideGUID = ValidPlanetSideGUID(guid)
+
+ def unapply(n : PlanetSideGUID) : Option[Int] = Some(n.guid)
+
+ implicit val codec = uint16L.xmap[PlanetSideGUID] (
+ n => PlanetSideGUID(n),
+ n => n.guid
+ )
}
diff --git a/common/src/test/scala/objects/DeployableTest.scala b/common/src/test/scala/objects/DeployableTest.scala
index a5581f0a..813ae7c9 100644
--- a/common/src/test/scala/objects/DeployableTest.scala
+++ b/common/src/test/scala/objects/DeployableTest.scala
@@ -15,16 +15,16 @@ class DeployableTest extends Specification {
"Deployable" should {
"know its owner by GUID" in {
val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine)
- obj.Owner mustEqual None
+ obj.Owner.isEmpty mustEqual true
obj.Owner = PlanetSideGUID(10)
- obj.Owner mustEqual Some(PlanetSideGUID(10))
+ obj.Owner.contains(PlanetSideGUID(10)) mustEqual true
}
"know its owner by GUID" in {
val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine)
- obj.OwnerName mustEqual None
+ obj.OwnerName.isEmpty mustEqual true
obj.OwnerName = "TestCharacter"
- obj.OwnerName mustEqual Some("TestCharacter")
+ obj.OwnerName.contains("TestCharacter") mustEqual true
}
"know its faction allegiance" in {
@@ -66,7 +66,7 @@ class BoomerDeployableTest extends Specification {
"construct" in {
val obj = new BoomerDeployable(GlobalDefinitions.boomer)
obj.Exploded mustEqual false
- obj.Trigger mustEqual None
+ obj.Trigger.isEmpty mustEqual true
}
"explode" in {
@@ -78,14 +78,14 @@ class BoomerDeployableTest extends Specification {
"manage its trigger" in {
val obj = new BoomerDeployable(GlobalDefinitions.boomer)
- obj.Trigger mustEqual None
+ obj.Trigger.isEmpty mustEqual true
val trigger = new BoomerTrigger
obj.Trigger = trigger
- obj.Trigger mustEqual Some(trigger)
+ obj.Trigger.contains(trigger) mustEqual true
obj.Trigger = None
- obj.Trigger mustEqual None
+ obj.Trigger.isEmpty mustEqual true
}
}
}
@@ -322,7 +322,7 @@ class TelepadDeployableTest extends Specification {
"construct" in {
val obj = new Telepad(GlobalDefinitions.router_telepad)
obj.Active mustEqual false
- obj.Router mustEqual None
+ obj.Router.isEmpty mustEqual true
}
"activate and deactivate" in {
@@ -336,15 +336,15 @@ class TelepadDeployableTest extends Specification {
"keep track of a Router" in {
val obj = new Telepad(GlobalDefinitions.router_telepad)
- obj.Router mustEqual None
+ obj.Router.isEmpty mustEqual true
obj.Router = PlanetSideGUID(1)
- obj.Router mustEqual Some(PlanetSideGUID(1))
+ obj.Router.contains(PlanetSideGUID(1)) mustEqual true
obj.Router = None
- obj.Router mustEqual None
+ obj.Router.isEmpty mustEqual true
obj.Router = PlanetSideGUID(1)
- obj.Router mustEqual Some(PlanetSideGUID(1))
+ obj.Router.contains(PlanetSideGUID(1)) mustEqual true
obj.Router = PlanetSideGUID(0)
- obj.Router mustEqual None
+ obj.Router.isEmpty mustEqual true
}
}
}
diff --git a/common/src/test/scala/objects/EntityTest.scala b/common/src/test/scala/objects/EntityTest.scala
index 92d3636a..b175611f 100644
--- a/common/src/test/scala/objects/EntityTest.scala
+++ b/common/src/test/scala/objects/EntityTest.scala
@@ -4,7 +4,7 @@ package objects
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.entity.NoGUIDException
-import net.psforever.types.{PlanetSideGUID, Vector3}
+import net.psforever.types.{PlanetSideGUID, StalePlanetSideGUID, ValidPlanetSideGUID, Vector3}
import org.specs2.mutable._
class EntityTest extends Specification {
@@ -13,6 +13,44 @@ class EntityTest extends Specification {
def Definition : ObjectDefinition = new ObjectDefinition(0) { }
}
+ "PlanetSideGUID" should {
+ "construct as valid" in {
+ ValidPlanetSideGUID(1).isInstanceOf[PlanetSideGUID] mustEqual true
+ }
+
+ "construct as stale" in {
+ StalePlanetSideGUID(1).isInstanceOf[PlanetSideGUID] mustEqual true
+ }
+
+ "apply construct (as valid)" in {
+ val guid = PlanetSideGUID(1)
+ guid.isInstanceOf[PlanetSideGUID] mustEqual true
+ guid.isInstanceOf[ValidPlanetSideGUID] mustEqual true
+ guid.isInstanceOf[StalePlanetSideGUID] mustEqual false
+ }
+
+ "valid and stale are equal by guid" in {
+ //your linter will complain; let it
+ ValidPlanetSideGUID(1) == StalePlanetSideGUID(1) mustEqual true
+ ValidPlanetSideGUID(1) == StalePlanetSideGUID(2) mustEqual false
+ }
+
+ "valid and stale are pattern-matchable" in {
+ val guid1 : PlanetSideGUID = ValidPlanetSideGUID(1)
+ val guid2 : PlanetSideGUID = StalePlanetSideGUID(1)
+ def getGuid(o : PlanetSideGUID) : PlanetSideGUID = o //distancing the proper type
+
+ getGuid(guid1) match {
+ case ValidPlanetSideGUID(1) => ok
+ case _ => ko
+ }
+ getGuid(guid2) match {
+ case StalePlanetSideGUID(1) => ok
+ case _ => ko
+ }
+ }
+ }
+
"SimpleWorldEntity" should {
"construct" in {
new EntityTestClass()
@@ -107,64 +145,129 @@ class EntityTest extends Specification {
"error while not set" in {
val obj : EntityTestClass = new EntityTestClass
- obj.HasGUID mustEqual false
obj.GUID must throwA[NoGUIDException]
}
- "work after mutation" in {
+ "error if set to an invalid GUID before being set to a valid GUID" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.GUID must throwA[NoGUIDException]
+ (obj.GUID = StalePlanetSideGUID(1)) must throwA[NoGUIDException]
+ }
+
+ "work after valid mutation" in {
val obj : EntityTestClass = new EntityTestClass
obj.GUID = PlanetSideGUID(1051)
- obj.HasGUID mustEqual true
obj.GUID mustEqual PlanetSideGUID(1051)
}
- "work after multiple mutations" in {
+ "ignore subsequent mutations using a valid GUID" in {
val obj : EntityTestClass = new EntityTestClass
obj.GUID = PlanetSideGUID(1051)
obj.GUID mustEqual PlanetSideGUID(1051)
- obj.GUID = PlanetSideGUID(30052)
- obj.GUID mustEqual PlanetSideGUID(30052)
- obj.GUID = PlanetSideGUID(62)
- obj.GUID mustEqual PlanetSideGUID(62)
+ obj.GUID = ValidPlanetSideGUID(1)
+ obj.GUID mustEqual PlanetSideGUID(1051)
}
- "invalidate and report as not having a GUID, but continue to work" in {
+ "ignore subsequent mutations using an invalid GUID" in {
val obj : EntityTestClass = new EntityTestClass
obj.GUID = PlanetSideGUID(1051)
- obj.HasGUID mustEqual true
obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID = StalePlanetSideGUID(1)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ }
+
+ "invalidate does nothing by default" in {
+ val obj : EntityTestClass = new EntityTestClass
obj.Invalidate()
- obj.HasGUID mustEqual false
- obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID must throwA[NoGUIDException]
}
- "assign a new GUID after invalidation and continue to work" in {
+ "invalidate changes the nature of the previous valid mutation" in {
val obj : EntityTestClass = new EntityTestClass
obj.GUID = PlanetSideGUID(1051)
- obj.HasGUID mustEqual true
obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[ValidPlanetSideGUID] mustEqual true
obj.Invalidate()
obj.GUID mustEqual PlanetSideGUID(1051)
- obj.HasGUID mustEqual false
-
- obj.GUID = PlanetSideGUID(1052)
- obj.HasGUID mustEqual true
- obj.GUID mustEqual PlanetSideGUID(1052)
+ obj.GUID.isInstanceOf[StalePlanetSideGUID] mustEqual true
}
- "assignthe same GUID after invalidation and continue to work" in {
+ "setting an invalid GUID after invalidating the previous valid mutation returns the same" in {
val obj : EntityTestClass = new EntityTestClass
- val guid = new PlanetSideGUID(1051)
- obj.GUID = guid
- obj.HasGUID mustEqual true
- obj.GUID mustEqual guid
+ obj.GUID = PlanetSideGUID(1051)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[ValidPlanetSideGUID] mustEqual true
obj.Invalidate()
- obj.GUID mustEqual guid
- obj.HasGUID mustEqual false
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[StalePlanetSideGUID] mustEqual true
+ obj.GUID = StalePlanetSideGUID(2)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[StalePlanetSideGUID] mustEqual true
+ }
- obj.GUID = guid
+ "setting a valid GUID after invalidating correctly sets the new valid GUID" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.GUID = PlanetSideGUID(1051)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[ValidPlanetSideGUID] mustEqual true
+ obj.Invalidate()
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[StalePlanetSideGUID] mustEqual true
+ obj.GUID = PlanetSideGUID(2)
+ obj.GUID mustEqual PlanetSideGUID(2)
+ obj.GUID.isInstanceOf[ValidPlanetSideGUID] mustEqual true
+ }
+
+ "setting the same valid GUID after invalidating correctly resets the valid GUID" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.GUID = PlanetSideGUID(1051)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[ValidPlanetSideGUID] mustEqual true
+ obj.Invalidate()
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[StalePlanetSideGUID] mustEqual true
+ obj.GUID = PlanetSideGUID(1051)
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ obj.GUID.isInstanceOf[ValidPlanetSideGUID] mustEqual true
+ }
+
+ "report not having a GUID when not set" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.HasGUID mustEqual false
+ }
+
+ "report having a GUID when a valid GUID is set" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.HasGUID mustEqual false
+ obj.GUID = PlanetSideGUID(1051)
+ obj.HasGUID mustEqual true
+ }
+
+ "report not having a GUID after invalidating (staleness)" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.HasGUID mustEqual false
+ obj.GUID = PlanetSideGUID(1051)
+ obj.HasGUID mustEqual true
+ obj.Invalidate()
+ obj.HasGUID mustEqual false
+ //remember that we will still return a GUID in this case
+ obj.GUID mustEqual PlanetSideGUID(1051)
+ }
+
+ "report having a GUID after setting a valid GUID, after invalidating" in {
+ val obj : EntityTestClass = new EntityTestClass
+ obj.HasGUID mustEqual false
+ obj.GUID = PlanetSideGUID(1051)
+ obj.HasGUID mustEqual true
+ obj.Invalidate()
+ obj.HasGUID mustEqual false
+ obj.GUID = PlanetSideGUID(2)
obj.HasGUID mustEqual true
- obj.GUID mustEqual guid
}
}
+
+ "hachCode test" in {
+ ValidPlanetSideGUID(1051).hashCode mustEqual ValidPlanetSideGUID(1051).hashCode
+ ValidPlanetSideGUID(1051).hashCode mustEqual StalePlanetSideGUID(1051).hashCode
+ }
}