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 + } }