Lightweight Locker (#578)

* object registration adjustments for players and avatars and lockers (especially lockers)

* refactored locker container

* modifications to fields and method names for guid-related files; SpecificNumberSource created

* locker item display; proper item insertion into locker-space and searchability of that locker-space

* proper item removal from locker-space, including swap-items on other insertion tasks

* comments and tests; adjusted avatar/player registrations; allowed for restriction in the SpecificNumberSource; renamed CataloguedInventory to LocallyRegisteredInventory, and made internal object registration work

* accommodations for RequestDestroy, to allow the locker's Clear button to work; modification of expectation for resolving projectiles through ValidObject
This commit is contained in:
Fate-JH 2020-09-15 19:46:56 -04:00 committed by GitHub
parent 8245d3ff1e
commit 7626c8e6c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1743 additions and 948 deletions

View file

@ -8,8 +8,11 @@ import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
import akka.pattern.ask
import akka.util.Timeout
import java.util.concurrent.TimeUnit
import MDCContextAware.Implicits._
import net.psforever.objects.locker.LockerContainer
import org.log4s.MDC
import scala.collection.mutable
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
@ -359,13 +362,29 @@ class SessionActor extends Actor with MDCContextAware {
def ValidObject(id: Option[PlanetSideGUID]): Option[PlanetSideGameObject] =
continent.GUID(id) match {
case Some(obj: LocalProjectile) =>
FindProjectileEntry(id.get)
case Some(_: LocalLockerItem) =>
player.avatar.locker.Inventory.hasItem(id.get) match {
case out @ Some(_) =>
out
case None =>
//delete stale entity reference from client
log.warn(s"${player.Name} has an invalid reference to GUID ${id.get} in zone ${continent.id}")
sendResponse(ObjectDeleteMessage(id.get, 0))
None
}
case out @ Some(obj) if obj.HasGUID =>
out
case None if id.nonEmpty && id.get != PlanetSideGUID(0) =>
//delete stale entity reference from client
log.warn(s"Player ${player.Name} has an invalid reference to GUID ${id.get} in zone ${continent.id}.")
log.warn(s"${player.Name} has an invalid reference to GUID ${id.get} in zone ${continent.id}")
sendResponse(ObjectDeleteMessage(id.get, 0))
None
case _ =>
None
}
@ -1893,7 +1912,7 @@ class SessionActor extends Actor with MDCContextAware {
continent.GUID(mount) match {
case Some(obj: Vehicle) =>
TotalDriverVehicleControl(obj)
UnAccessContents(obj)
UnaccessContainer(obj)
case _ => ;
}
PlayerActionsToCancel()
@ -2480,7 +2499,7 @@ class SessionActor extends Actor with MDCContextAware {
// the player will receive no messages consistently except the KeepAliveMessage echo
keepAliveFunc = KeepAlivePersistence
}
AccessContents(obj)
AccessContainer(obj)
UpdateWeaponAtSeatPosition(obj, seat_num)
MountingAction(tplayer, obj, seat_num)
@ -2516,7 +2535,7 @@ class SessionActor extends Actor with MDCContextAware {
DismountAction(tplayer, obj, seat_num)
case Mountable.CanDismount(obj: Vehicle, seat_num) if obj.Definition == GlobalDefinitions.droppod =>
UnAccessContents(obj)
UnaccessContainer(obj)
DismountAction(tplayer, obj, seat_num)
case Mountable.CanDismount(obj: Vehicle, seat_num) =>
@ -2524,7 +2543,7 @@ class SessionActor extends Actor with MDCContextAware {
if (player_guid == player.GUID) {
//disembarking self
TotalDriverVehicleControl(obj)
UnAccessContents(obj)
UnaccessContainer(obj)
DismountAction(tplayer, obj, seat_num)
} else {
continent.VehicleEvents ! VehicleServiceMessage(
@ -2735,7 +2754,7 @@ class SessionActor extends Actor with MDCContextAware {
if (tplayer_guid == guid) {
continent.GUID(vehicle_guid) match {
case Some(obj: Vehicle) =>
UnAccessContents(obj)
UnaccessContainer(obj)
case _ => ;
}
}
@ -3779,9 +3798,7 @@ class SessionActor extends Actor with MDCContextAware {
val guid = player.GUID
sendResponse(UnuseItemMessage(guid, veh.GUID))
sendResponse(UnuseItemMessage(guid, guid))
veh.AccessingTrunk = None
UnAccessContents(veh)
accessedContainer = None
UnaccessContainer(veh)
}
case Some(container) => //just in case
if (isMovingPlus) {
@ -3791,7 +3808,7 @@ class SessionActor extends Actor with MDCContextAware {
sendResponse(UnuseItemMessage(guid, container.GUID))
}
sendResponse(UnuseItemMessage(guid, guid))
accessedContainer = None
UnaccessContainer(container)
}
case None => ;
}
@ -4384,25 +4401,20 @@ class SessionActor extends Actor with MDCContextAware {
case Some(obj: Equipment) =>
FindEquipmentToDelete(object_guid, obj)
case Some(_: LocalProjectile) =>
FindProjectileEntry(object_guid) match {
case Some(projectile) =>
if (projectile.isResolved) {
log.warn(
s"RequestDestroy: tried to clean up projectile ${object_guid.guid} but it was already resolved"
)
} else {
projectile.Miss()
if (projectile.profile.ExistsOnRemoteClients && projectile.HasGUID) {
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.ProjectileExplodes(player.GUID, projectile.GUID, projectile)
)
taskResolver ! UnregisterProjectile(projectile)
}
}
case None =>
log.warn(s"RequestDestroy: projectile ${object_guid.guid} has never been fired")
case Some(obj: Projectile) =>
if (obj.isResolved) {
log.warn(
s"RequestDestroy: tried to clean up projectile ${object_guid.guid} but it was already resolved"
)
} else {
obj.Miss()
if (obj.profile.ExistsOnRemoteClients && obj.HasGUID) {
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.ProjectileExplodes(player.GUID, obj.GUID, obj)
)
taskResolver ! UnregisterProjectile(obj)
}
}
case Some(obj: BoomerDeployable) =>
@ -4463,7 +4475,7 @@ class SessionActor extends Actor with MDCContextAware {
Some(destination: PlanetSideServerObject with Container),
Some(item: Equipment)
) =>
source.Actor ! Containable.MoveItem(destination, item, dest)
ContainableMoveItem(taskResolver, player.Name, source, destination, item, dest)
case (None, _, _) =>
log.error(s"MoveItem: wanted to move $item_guid from $source_guid, but could not find source object")
case (_, None, _) =>
@ -4500,7 +4512,7 @@ class SessionActor extends Actor with MDCContextAware {
destination.Fit(item)
) match {
case (Some((source, Some(_))), Some(dest)) =>
source.Actor ! Containable.MoveItem(destination, item, dest)
ContainableMoveItem(taskResolver, player.Name, source, destination, item, dest)
case (None, _) =>
log.error(s"LootItem: can not find where $item is put currently")
case (_, None) =>
@ -4610,7 +4622,7 @@ class SessionActor extends Actor with MDCContextAware {
itemType
)
)
accessedContainer = Some(obj)
AccessContainer(obj)
}
} else if (!unk3 && player.isAlive) { //potential kit use
ValidObject(item_used_guid) match {
@ -4759,13 +4771,12 @@ class SessionActor extends Actor with MDCContextAware {
case None if locker.Faction == player.Faction || !locker.HackedBy.isEmpty =>
log.trace(s"UseItem: ${player.Name} accessing a locker")
CancelZoningProcessWithDescriptiveReason("cancel_use")
val container = player.avatar.locker
accessedContainer = Some(container)
val playerLocker = player.avatar.locker
sendResponse(
UseItemMessage(
avatar_guid,
item_used_guid,
container.GUID,
playerLocker.GUID,
unk2,
unk3,
unk4,
@ -4776,6 +4787,7 @@ class SessionActor extends Actor with MDCContextAware {
456
)
)
AccessContainer(playerLocker)
case _ => ;
}
@ -4827,8 +4839,7 @@ class SessionActor extends Actor with MDCContextAware {
) {
CancelZoningProcessWithDescriptiveReason("cancel_use")
obj.AccessingTrunk = player.GUID
accessedContainer = Some(obj)
AccessContents(obj)
AccessContainer(obj)
sendResponse(
UseItemMessage(
avatar_guid,
@ -5044,19 +5055,16 @@ class SessionActor extends Actor with MDCContextAware {
case msg @ UnuseItemMessage(player_guid, object_guid) =>
log.info(s"UnuseItem: $msg")
//TODO check for existing accessedContainer value?
ValidObject(object_guid) match {
case Some(obj: Vehicle) =>
if (obj.AccessingTrunk.contains(player.GUID)) {
obj.AccessingTrunk = None
UnAccessContents(obj)
}
case Some(obj: Player) =>
UnaccessContainer(obj)
TryDisposeOfLootedCorpse(obj)
case Some(obj: Container) =>
UnaccessContainer(obj)
case _ => ;
}
accessedContainer = None
case msg @ DeployObjectMessage(guid, unk1, pos, orient, unk2) =>
log.info(s"DeployObject: $msg")
@ -5757,7 +5765,8 @@ class SessionActor extends Actor with MDCContextAware {
}
/**
* Construct tasking that registers all aspects of a `Player` avatar.
* Construct tasking that registers all aspects of a `Player` avatar
* as if that player is only just being introduced.
* `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
* @param tplayer the avatar `Player`
* @return a `TaskResolver.GiveTask` message
@ -5785,7 +5794,7 @@ class SessionActor extends Actor with MDCContextAware {
}
override def onFailure(ex: Throwable): Unit = {
localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WorldSessionActor
localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts SessionActor
}
},
List(GUIDTask.RegisterAvatar(tplayer)(continent.GUID))
@ -5793,7 +5802,8 @@ class SessionActor extends Actor with MDCContextAware {
}
/**
* Construct tasking that registers all aspects of a `Player` avatar.
* Construct tasking that registers all aspects of a `Player` avatar
* as if that player was already introduced and is just being renewed.
* `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
* @param tplayer the avatar `Player`
* @return a `TaskResolver.GiveTask` message
@ -5872,7 +5882,7 @@ class SessionActor extends Actor with MDCContextAware {
* Additionally, the driver is only partially associated with the vehicle at this time.
* `interstellarFerry` is properly keeping track of the vehicle during the transition
* and the user who is the driver (second param) is properly seated
* but the said driver does not know about the vehicle through his usual convention - VehicleSeated` - yet.
* but the said driver does not know about the vehicle through his usual convention - `VehicleSeated` - yet.
* @see `GlobalDefinitions.droppod`
* @see `GUIDTask.RegisterObjectTask`
* @see `interstellarFerry`
@ -5975,10 +5985,10 @@ class SessionActor extends Actor with MDCContextAware {
}
override def onFailure(ex: Throwable): Unit = {
localAnnounce ! PlayerFailedToLoad(localDriver) //alerts WorldSessionActor
localAnnounce ! PlayerFailedToLoad(localDriver) //alerts SessionActor
}
},
List(GUIDTask.RegisterAvatar(driver)(continent.GUID), GUIDTask.RegisterVehicle(obj)(continent.GUID))
List(RegisterNewAvatar(driver), GUIDTask.RegisterVehicle(obj)(continent.GUID))
)
}
@ -6115,50 +6125,106 @@ class SessionActor extends Actor with MDCContextAware {
)
}
def AccessContainer(container: Container): Unit = {
container match {
case v: Vehicle =>
AccessVehicleContents(v)
case o: LockerContainer =>
AccessGenericContainer(o)
case p: PlanetSideServerObject with Container =>
accessedContainer = Some(p)
case _ => ;
}
}
def AccessGenericContainer(container: PlanetSideServerObject with Container): Unit = {
accessedContainer = Some(container)
DisplayContainerContents(container.GUID, container)
}
/**
* Common preparation for interfacing with a vehicle.
* Join a vehicle-specific group for shared updates.
* Construct every object in the vehicle's inventory for shared manipulation updates.
* @param vehicle the vehicle
*/
def AccessContents(vehicle: Vehicle): Unit = {
AccessContentsChannel(vehicle)
val parent_guid = vehicle.GUID
vehicle.Trunk.Items.foreach(entry => {
val obj = entry.obj
val objDef = obj.Definition
sendResponse(
ObjectCreateDetailedMessage(
objDef.ObjectId,
obj.GUID,
ObjectCreateMessageParent(parent_guid, entry.start),
objDef.Packet.DetailedConstructorData(obj).get
)
)
})
def AccessVehicleContents(vehicle: Vehicle): Unit = {
accessedContainer = Some(vehicle)
if(vehicle.AccessingTrunk.isEmpty) {
vehicle.AccessingTrunk = Some(player.GUID)
}
AccessVehicleContainerChannel(vehicle)
DisplayContainerContents(vehicle.GUID, vehicle)
}
def AccessContentsChannel(container: PlanetSideServerObject): Unit = {
def AccessVehicleContainerChannel(container: PlanetSideServerObject): Unit = {
continent.VehicleEvents ! Service.Join(s"${container.Actor}")
}
def DisplayContainerContents(containerId: PlanetSideGUID, container: Container): Unit = {
container.Inventory.Items.foreach(entry => {
val obj = entry.obj
val objDef = obj.Definition
sendResponse(
ObjectCreateDetailedMessage(
objDef.ObjectId,
obj.GUID,
ObjectCreateMessageParent(containerId, entry.start),
objDef.Packet.DetailedConstructorData(obj).get
)
)
})
}
def UnaccessContainer(): Unit = {
accessedContainer match {
case Some(container) => UnaccessContainer(container)
case _ => ;
}
}
def UnaccessContainer(container: Container): Unit = {
container match {
case v: Vehicle =>
UnaccessVehicleContainer(v)
case o: LockerContainer =>
UnaccessGenericContainer(o)
case _: PlanetSideServerObject with Container =>
accessedContainer = None
case _ => ;
}
}
def UnaccessGenericContainer(container: Container): Unit = {
accessedContainer = None
HideContainerContents(container)
}
/**
* Common preparation for disengaging from a vehicle.
* Leave the vehicle-specific group that was used for shared updates.
* Deconstruct every object in the vehicle's inventory.
* @param vehicle the vehicle
*/
def UnAccessContents(vehicle: Vehicle): Unit = {
continent.VehicleEvents ! Service.Leave(Some(s"${vehicle.Actor}"))
vehicle.Trunk.Items.foreach(entry => {
sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0))
})
def UnaccessVehicleContainer(vehicle: Vehicle): Unit = {
accessedContainer = None
if(vehicle.AccessingTrunk.contains(player.GUID)) {
vehicle.AccessingTrunk = None
}
UnaccessVehicleContainerChannel(vehicle)
HideContainerContents(vehicle)
}
def UnAccessContentsChannel(container: PlanetSideServerObject): Unit = {
def UnaccessVehicleContainerChannel(container: PlanetSideServerObject): Unit = {
continent.VehicleEvents ! Service.Leave(Some(s"${container.Actor}"))
}
def HideContainerContents(container: Container): Unit = {
container.Inventory.Items.foreach(entry => {
sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0))
})
}
/**
* Check two locations for a controlled piece of equipment that is associated with the `player`.<br>
* <br>
@ -6813,19 +6879,7 @@ class SessionActor extends Actor with MDCContextAware {
progressBarUpdate.cancel()
progressBarValue = None
lastTerminalOrderFulfillment = true
accessedContainer match {
case Some(obj: Vehicle) =>
if (obj.AccessingTrunk.contains(player.GUID)) {
obj.AccessingTrunk = None
UnAccessContents(obj)
}
accessedContainer = None
case Some(_) =>
accessedContainer = None
case None => ;
}
UnaccessContainer()
prefire.orElse(shooting) match {
case Some(guid) =>
sendResponse(ChangeFireStateMessage_Stop(guid))
@ -7030,7 +7084,7 @@ class SessionActor extends Actor with MDCContextAware {
* and is permitted to introduce the avatar to the vehicle's internal settings in a similar way.
* Neither the player avatar nor the vehicle should be reconstructed before the next zone load operation
* to avoid damaging the critical setup of this function.
* @see `AccessContents`
* @see `AccessContainer`
* @see `UpdateWeaponAtSeatPosition`
* @param tplayer the player avatar seated in the vehicle's seat
* @param vehicle the vehicle the player is riding
@ -7046,7 +7100,7 @@ class SessionActor extends Actor with MDCContextAware {
sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata))
if (seat == 0 || vehicle.Seats(seat).ControlledWeapon.nonEmpty) {
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
AccessContents(vehicle)
AccessContainer(vehicle)
UpdateWeaponAtSeatPosition(vehicle, seat)
} else {
interimUngunnedVehicle = Some(vguid)
@ -7120,7 +7174,7 @@ class SessionActor extends Actor with MDCContextAware {
//log.info(s"AvatarRejoin: $vguid -> $vdata")
if (seat == 0 || vehicle.Seats(seat).ControlledWeapon.nonEmpty) {
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
AccessContents(vehicle)
AccessContainer(vehicle)
UpdateWeaponAtSeatPosition(vehicle, seat)
} else {
interimUngunnedVehicle = Some(vguid)
@ -8203,6 +8257,7 @@ class SessionActor extends Actor with MDCContextAware {
/**
* A simple object searching algorithm that is limited to containers currently known and accessible by the player.
* If all relatively local containers are checked and the object is not found,
* the player's locker inventory will be checked, and then
* the game environment (items on the ground) will be checked too.
* If the target object is discovered, it is removed from its current location and is completely destroyed.
* @see `RequestDestroyMessage`<br>
@ -8218,8 +8273,7 @@ class SessionActor extends Actor with MDCContextAware {
: PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] =
FindInLocalContainer(object_guid)
findFunc(player.avatar.locker)
.orElse(findFunc(player))
findFunc(player)
.orElse(accessedContainer match {
case Some(parent: PlanetSideServerObject) =>
findFunc(parent)
@ -8239,7 +8293,11 @@ class SessionActor extends Actor with MDCContextAware {
true
case _ =>
if (continent.EquipmentOnGround.contains(obj)) {
if (player.avatar.locker.Inventory.Remove(object_guid)) {
sendResponse(ObjectDeleteMessage(object_guid, 0))
log.info(s"RequestDestroy: equipment $obj")
true
} else if (continent.EquipmentOnGround.contains(obj)) {
obj.Position = Vector3.Zero
continent.Ground ! Zone.Ground.RemoveItem(object_guid)
continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
@ -8440,7 +8498,7 @@ class SessionActor extends Actor with MDCContextAware {
if (player.isBackpack) {
session = session.copy(player = targetPlayer)
taskThenZoneChange(
GUIDTask.UnregisterLocker(original.avatar.locker)(continent.GUID),
GUIDTask.UnregisterObjectTask(original.avatar.locker)(continent.GUID),
InterstellarClusterService.FindZone(_.id == zoneId, context.self)
)
} else if (player.HasGUID) {
@ -8545,12 +8603,12 @@ class SessionActor extends Actor with MDCContextAware {
InterstellarClusterService.FindZone(_.id == zoneId, context.self)
)
} else {
UnAccessContents(vehicle)
UnaccessContainer(vehicle)
LoadZoneCommonTransferActivity()
player.VehicleSeated = vehicle.GUID
player.Continent = zoneId //forward-set the continent id to perform a test
interstellarFerryTopLevelGUID =
(if (
if (
manifest.passengers.isEmpty && manifest.cargo.count { case (name, _) => !name.equals("MISSING_DRIVER") } == 0
) {
//do not delete if vehicle has passengers or cargo
@ -8561,7 +8619,7 @@ class SessionActor extends Actor with MDCContextAware {
None
} else {
Some(topLevel)
})
}
//unregister vehicle and driver whole + GiveWorld
continent.Transport ! Zone.Vehicle.Despawn(vehicle)
taskThenZoneChange(
@ -8586,12 +8644,12 @@ class SessionActor extends Actor with MDCContextAware {
* A reference to the top-level ferrying vehicle's former globally unique identifier has been retained for this purpose.
* This vehicle can be deleted for everyone if no more work can be detected.
*
* @see `GUIDTask.UnregisterAvatar`
* @see `GUIDTask.UnregisterPlayer`
* @see `LoadZoneCommonTransferActivity`
* @see `Vehicles.AllGatedOccupantsInSameZone`
* @see `PlayerLoaded`
* @see `TaskBeforeZoneChange`
* @see `UnAccessContents`
* @see `UnaccessContainer`
* @param vehicle the target vehicle being moved around
* @param zoneId the zone in which the vehicle and driver will be placed
* @return a tuple composed of an `ActorRef` destination and a message to send to that destination
@ -9209,7 +9267,7 @@ class SessionActor extends Actor with MDCContextAware {
}
/**
* The upstream counter accumulates when the server receives sp[ecific messages from the client.
* The upstream counter accumulates when the server receives specific messages from the client.
* It counts upwards until it reach maximum value, and then starts over.
* When it starts over, which should take an exceptionally long time to achieve,
* it starts counting at one rather than zero.
@ -9265,7 +9323,7 @@ class SessionActor extends Actor with MDCContextAware {
case (Some(vehicle: Vehicle), Some(vguid), Some(seat)) =>
//sit down
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
AccessContents(vehicle)
AccessContainer(vehicle)
keepAliveFunc = KeepAlivePersistence
case _ => ;
//we can't find a vehicle? and we're still here? that's bad

View file

@ -6,6 +6,7 @@ import akka.util.Timeout
import net.psforever.objects.equipment.{Ammo, Equipment}
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.zones.Zone
@ -504,6 +505,215 @@ object WorldSession {
result
}
/**
* Move an item from one container to another.
* If the source or if the destination is a kind of container called a `LockerContainer`,
* then a special procedure for the movement of the item must be respected.
* If the source and the destination are both `LockerContainer` objects, however,
* the normal operations for moving an item may be executed.
* @see `ActorRef`
* @see `Containable.MoveItem`
* @see `Container`
* @see `Equipment`
* @see `LockerContainer`
* @see `RemoveEquipmentFromLockerContainer`
* @see `StowEquipmentInLockerContainer`
* @see `TaskResolver`
* @param taskResolver na
* @param toChannel broadcast channel name for a manual packet callback
* @param source the container in which the item is to be removed
* @param destination the container into which the item is to be placed
* @param item the item
* @param dest where in the destination container the item is being placed
*/
def ContainableMoveItem(
taskResolver: ActorRef,
toChannel: String,
source: PlanetSideServerObject with Container,
destination: PlanetSideServerObject with Container,
item: Equipment,
dest: Int
) : Unit = {
(source, destination) match {
case (locker: LockerContainer, _) if !destination.isInstanceOf[LockerContainer] =>
RemoveEquipmentFromLockerContainer(taskResolver, toChannel, locker, destination, item, dest)
case (_, locker: LockerContainer) =>
StowEquipmentInLockerContainer(taskResolver, toChannel, source, locker, item, dest)
case _ =>
source.Actor ! Containable.MoveItem(destination, item, dest)
}
}
/**
* Move an item into a player's locker inventory.
* Handle any swap item that might become involved in the transfer.
* Failure of this process is not supported and may lead to irregular behavior.
* @see `ActorRef`
* @see `AvatarAction.ObjectDelete`
* @see `AvatarServiceMessage`
* @see `Containable.MoveItem`
* @see `Container`
* @see `Equipment`
* @see `GridInventory.CheckCollisionsVar`
* @see `GUIDTask.RegisterEquipment`
* @see `GUIDTask.UnregisterEquipment`
* @see `IdentifiableEntity.Invalidate`
* @see `LockerContainer`
* @see `Service`
* @see `Task`
* @see `TaskResolver`
* @see `TaskResolver.GiveTask`
* @see `Zone.AvatarEvents`
* @param taskResolver na
* @param toChannel broadcast channel name for a manual packet callback
* @param source the container in which the item is to be removed
* @param destination the container into which the item is to be placed
* @param item the item
* @param dest where in the destination container the item is being placed
*/
def StowEquipmentInLockerContainer(
taskResolver: ActorRef,
toChannel: String,
source: PlanetSideServerObject with Container,
destination: PlanetSideServerObject with Container,
item: Equipment,
dest: Int
): Unit = {
val registrationTask = GUIDTask.UnregisterEquipment(item)(source.Zone.GUID)
//check for the existence of a swap item - account for that in advance
val (subtasks, swapItemGUID): (List[TaskResolver.GiveTask], Option[PlanetSideGUID]) = {
val tile = item.Definition.Tile
destination.Inventory.CheckCollisionsVar(dest, tile.Width, tile.Height)
} match {
case Success(Nil) =>
//no swap item
(List(registrationTask), None)
case Success(List(swapEntry: InventoryItem)) =>
//the swap item is to be registered to the source's zone
/*
destination is a locker container that has its own internal unique number system
the swap item is currently registered to this system
the swap item will be moved into the system in which the source operates
to facilitate the transfer, the item needs to be partially unregistered from the destination's system
to facilitate the transfer, the item needs to be preemptively registered to the source's system
invalidating the current unique number is sufficient for both of these steps
*/
val swapItem = swapEntry.obj
swapItem.Invalidate()
(List(GUIDTask.RegisterEquipment(swapItem)(source.Zone.GUID), registrationTask), Some(swapItem.GUID))
case _ =>
//too many swap items or other error; this attempt will probably fail
(Nil, None)
}
taskResolver ! TaskResolver.GiveTask(
new Task() {
val localGUID = swapItemGUID //the swap item's original GUID, if any swap item
val localChannel = toChannel
val localSource = source
val localDestination = destination
val localItem = item
val localSlot = dest
override def Description: String = s"unregistering $localItem before stowing in $localDestination"
override def isComplete: Task.Resolution.Value = {
if (localItem.HasGUID && localDestination.Find(localItem).contains(localSlot)) {
Task.Resolution.Success
} else {
Task.Resolution.Incomplete
}
}
def Execute(resolver: ActorRef): Unit = {
localGUID match {
case Some(guid) =>
//see LockerContainerControl.RemoveItemFromSlotCallback
val zone = localSource.Zone
zone.AvatarEvents ! AvatarServiceMessage(localChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, guid))
case None => ;
}
localSource.Actor ! Containable.MoveItem(localDestination, localItem, localSlot)
resolver ! Success(this)
}
},
subtasks
)
}
/**
* Remove an item from a player's locker inventory.
* Failure of this process is not supported and may lead to irregular behavior.
* @see `ActorRef`
* @see `AvatarAction.ObjectDelete`
* @see `AvatarServiceMessage`
* @see `Containable.MoveItem`
* @see `Container`
* @see `Equipment`
* @see `GridInventory.CheckCollisionsVar`
* @see `GUIDTask.RegisterEquipment`
* @see `GUIDTask.UnregisterEquipment`
* @see `IdentifiableEntity.Invalidate`
* @see `LockerContainer`
* @see `Service`
* @see `Task`
* @see `TaskResolver`
* @see `TaskResolver.GiveTask`
* @see `Zone.AvatarEvents`
* @param taskResolver na
* @param toChannel broadcast channel name for a manual packet callback
* @param source the container in which the item is to be removed
* @param destination the container into which the item is to be placed
* @param item the item
* @param dest where in the destination container the item is being placed
*/
def RemoveEquipmentFromLockerContainer(
taskResolver: ActorRef,
toChannel: String,
source: PlanetSideServerObject with Container,
destination: PlanetSideServerObject with Container,
item: Equipment,
dest: Int
): Unit = {
taskResolver ! TaskResolver.GiveTask(
new Task() {
val localGUID = item.GUID //original GUID
val localChannel = toChannel
val localSource = source
val localDestination = destination
val localItem = item
val localSlot = dest
/*
source is a locker container that has its own internal unique number system
the item is currently registered to this system
the item will be moved into the system in which the destination operates
to facilitate the transfer, the item needs to be partially unregistered from the source's system
to facilitate the transfer, the item needs to be preemptively registered to the destination's system
invalidating the current unique number is sufficient for both of these steps
*/
localItem.Invalidate()
override def Description: String = s"registering $localItem in ${localDestination.Zone.id} before removing from $localSource"
override def isComplete: Task.Resolution.Value = {
if (localItem.HasGUID && localDestination.Find(localItem).isEmpty) {
Task.Resolution.Success
} else {
Task.Resolution.Incomplete
}
}
def Execute(resolver: ActorRef): Unit = {
val zone = localSource.Zone
//see LockerContainerControl.RemoveItemFromSlotCallback
zone.AvatarEvents ! AvatarServiceMessage(localChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, localGUID))
localSource.Actor ! Containable.MoveItem(localDestination, localItem, localSlot)
resolver ! Success(this)
}
},
List(GUIDTask.RegisterEquipment(item)(destination.Zone.GUID))
)
}
/**
* If a timeout occurs on the manipulation, declare a terminal transaction failure.
* @see `AskTimeoutException`

View file

@ -0,0 +1,37 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects
import akka.actor.ActorContext
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.types.PlanetSideEmpire
/**
* A `LocalLockerItem` is a server-side object designed to populate a fake unshared space within a shared space.
* It is a placeholder intended to block out the existence of locker objects that may or may not exist.
* All clients reserve the same internal range of user-generated GUID's from 40150 to 40449, inclusive.
* All clients recognize this same range independent of each other as "equipment in their own locker."
* The GUID's in this locker-space can be reflected onto the zone GUID.
* The item of that GUID may exist to the client.
* The item of that GUID does not formally exist to the server, but it can be searched.
* @see `Zone.MakeReservedObjects`
*/
class LocalLockerItem extends PlanetSideServerObject {
def Faction = PlanetSideEmpire.NEUTRAL
def Definition = LocalLockerItem.local
}
object LocalLockerItem {
import net.psforever.objects.definition.ObjectDefinition
def local = new ObjectDefinition(0) { Name = "locker-equipment" }
/**
* Instantiate and configure a `LocalProjectile` object.
* @param id the unique id that will be assigned to this entity
* @param context a context to allow the object to properly set up `ActorSystem` functionality
* @return the `LocalProjectile` object
*/
def Constructor(id: Int, context: ActorContext): LocalLockerItem = {
new LocalLockerItem()
}
}

View file

@ -2,8 +2,10 @@ package net.psforever.objects.avatar
import net.psforever.objects.definition.{AvatarDefinition, BasicDefinition}
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot}
import net.psforever.objects.inventory.LocallyRegisteredInventory
import net.psforever.objects.loadouts.{Loadout, SquadLoadout}
import net.psforever.objects.{GlobalDefinitions, LockerContainer, LockerEquipment, OffhandEquipmentSlot}
import net.psforever.objects.locker.{LockerContainer, LockerEquipment}
import net.psforever.objects.{GlobalDefinitions, OffhandEquipmentSlot}
import net.psforever.types._
import org.joda.time.{Duration, LocalDateTime, Seconds}
@ -82,7 +84,11 @@ case class Avatar(
loadouts: Seq[Option[Loadout]] = Seq.fill(15)(None),
squadLoadouts: Seq[Option[SquadLoadout]] = Seq.fill(10)(None),
implants: Seq[Option[Implant]] = Seq(None, None, None),
locker: LockerContainer = new LockerContainer(), // TODO var bad
locker: LockerContainer = new LockerContainer({
val inv = new LocallyRegisteredInventory(numbers = 40150 until 40450) // TODO var bad
inv.Resize(30,20)
inv
}),
deployables: DeployableToolbox = new DeployableToolbox(), // TODO var bad
lookingForSquad: Boolean = false,
var vehicle: Option[PlanetSideGUID] = None, // TODO var bad

View file

@ -27,6 +27,8 @@ import net.psforever.services.{RemoverActor, Service}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import akka.actor.typed
import net.psforever.objects.locker.LockerContainerControl
import scala.concurrent.duration._
class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Command])

View file

@ -4,7 +4,7 @@ package net.psforever.objects.definition.converter
import net.psforever.objects.Player
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.packet.game.objectcreate._
import net.psforever.types.{ExoSuitType, GrenadeState, PlanetSideGUID}
import net.psforever.types.{ExoSuitType, GrenadeState, PlanetSideEmpire, PlanetSideGUID}
import scala.annotation.tailrec
import scala.util.{Success, Try}
@ -175,7 +175,9 @@ object AvatarConverter {
def MakeDetailedInventoryData(obj: Player): InventoryData = {
InventoryData(
(MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)
(MakeHolsters(obj, BuildDetailedEquipment) ++
MakeFifthSlot(obj) ++
MakeInventory(obj)).sortBy(_.parentSlot)
)
}
@ -222,7 +224,16 @@ object AvatarConverter {
private def MakeFifthSlot(obj: Player): List[InternalSlot] = {
obj.Slot(5).Equipment match {
case Some(equip) =>
BuildDetailedEquipment(5, equip) :: Nil
//List(BuildDetailedEquipment(5, equip))
List(InternalSlot(
equip.Definition.ObjectId,
equip.GUID,
5,
DetailedLockerContainerData(
CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)),
None
)
))
case _ =>
Nil
}

View file

@ -1,9 +1,9 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition.converter
import net.psforever.objects.LockerEquipment
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.GridInventory
import net.psforever.objects.locker.LockerEquipment
import net.psforever.packet.game.objectcreate._
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}

View file

@ -6,6 +6,7 @@ import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects._
import net.psforever.objects.inventory.Container
import net.psforever.objects.locker.{LockerContainer, LockerEquipment}
import net.psforever.objects.serverobject.turret.WeaponTurret
import scala.annotation.tailrec
@ -153,7 +154,7 @@ object GUIDTask {
*/
def RegisterAvatar(tplayer: Player)(implicit guid: ActorRef): TaskResolver.GiveTask = {
val holsterTasks = VisibleSlotTaskBuilding(tplayer.Holsters(), RegisterEquipment)
val lockerTask = List(RegisterLocker(tplayer.avatar.locker))
val lockerTask = List(RegisterObjectTask(tplayer.avatar.locker))
val inventoryTasks = RegisterInventory(tplayer)
TaskResolver.GiveTask(RegisterObjectTask(tplayer).task, holsterTasks ++ lockerTask ++ inventoryTasks)
}
@ -311,7 +312,7 @@ object GUIDTask {
*/
def UnregisterAvatar(tplayer: Player)(implicit guid: ActorRef): TaskResolver.GiveTask = {
val holsterTasks = VisibleSlotTaskBuilding(tplayer.Holsters(), UnregisterEquipment)
val lockerTask = List(UnregisterLocker(tplayer.avatar.locker))
val lockerTask = List(UnregisterObjectTask(tplayer.avatar.locker))
val inventoryTasks = UnregisterInventory(tplayer)
TaskResolver.GiveTask(UnregisterObjectTask(tplayer).task, holsterTasks ++ lockerTask ++ inventoryTasks)
}
@ -368,7 +369,7 @@ object GUIDTask {
* @param guid implicit reference to a unique number system
* @return a list of `TaskResolver.GiveTask` messages
*/
def VisibleSlotTaskBuilding(list: Iterable[EquipmentSlot], func: ((Equipment) => TaskResolver.GiveTask))(implicit
def VisibleSlotTaskBuilding(list: Iterable[EquipmentSlot], func: Equipment => TaskResolver.GiveTask)(implicit
guid: ActorRef
): List[TaskResolver.GiveTask] = {
recursiveVisibleSlotTaskBuilding(list.iterator, func)
@ -386,7 +387,7 @@ object GUIDTask {
*/
@tailrec private def recursiveVisibleSlotTaskBuilding(
iter: Iterator[EquipmentSlot],
func: ((Equipment) => TaskResolver.GiveTask),
func: Equipment => TaskResolver.GiveTask,
list: List[TaskResolver.GiveTask] = Nil
)(implicit guid: ActorRef): List[TaskResolver.GiveTask] = {
if (!iter.hasNext) {

View file

@ -24,8 +24,8 @@ class NumberPoolHub(private val source: NumberSource) {
import scala.collection.mutable
private val hash: mutable.HashMap[String, NumberPool] = mutable.HashMap[String, NumberPool]()
private val bigpool: mutable.LongMap[String] = mutable.LongMap[String]()
hash += "generic" -> new GenericPool(bigpool, source.Size)
source.FinalizeRestrictions.foreach(i =>
hash += "generic" -> new GenericPool(bigpool, source.size)
source.finalizeRestrictions.foreach(i =>
bigpool += i.toLong -> ""
) //these numbers can never be pooled; the source can no longer restrict numbers
@ -47,7 +47,7 @@ class NumberPoolHub(private val source: NumberSource) {
* @param number the unique number to attempt to retrieve from the `source`
* @return the object that is assigned to the number
*/
def apply(number: Int): Option[IdentifiableEntity] = source.Get(number).orElse(return None).get.Object
def apply(number: Int): Option[IdentifiableEntity] = source.get(number).orElse(return None).get.Object
def Numbers: List[Int] = bigpool.keys.map(key => key.toInt).toList
@ -74,14 +74,15 @@ class NumberPoolHub(private val source: NumberSource) {
if (pool.isEmpty) {
throw new IllegalArgumentException(s"can not add empty pool $name")
}
if (source.Size <= pool.max) {
throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size")
if (source.max < pool.max) {
throw new IllegalArgumentException(s"can not add pool $name - pool.max is greater than source.max")
}
val collision = bigpool.keys.map(n => n.toInt).toSet.intersect(pool.toSet)
if (collision.nonEmpty) {
throw new IllegalArgumentException(
s"can not add pool $name - it contains the following redundant numbers: ${collision.toString}"
)
bigpool.keys.map(n => n.toInt).toSet.intersect(pool.toSet).toSeq match {
case Nil => ;
case collisions =>
throw new IllegalArgumentException(
s"can not add pool $name - it contains the following redundant numbers: ${collisions.mkString(",")}"
)
}
pool.foreach(i => bigpool += i.toLong -> name)
hash += name -> new ExclusivePool(pool)
@ -162,7 +163,7 @@ class NumberPoolHub(private val source: NumberSource) {
def WhichPool(obj: IdentifiableEntity): Option[String] = {
try {
val number: Int = obj.GUID.guid
val entry = source.Get(number)
val entry = source.get(number)
if (entry.isDefined && entry.get.Object.contains(obj)) { WhichPool(number) }
else { None }
} catch {
@ -228,7 +229,7 @@ class NumberPoolHub(private val source: NumberSource) {
}
private def register_GetAvailableNumberFromSource(number: Int): Try[LoanedKey] = {
source.Available(number) match {
source.getAvailable(number) match {
case Some(key) =>
Success(key)
case None =>
@ -243,22 +244,21 @@ class NumberPoolHub(private val source: NumberSource) {
* @return the number the was given to the object
*/
def register(obj: IdentifiableEntity, name: String): Try[Int] = {
try {
if (obj.HasGUID) {
register_CheckNumberAgainstDesiredPool(obj, name, obj.GUID.guid)
} catch {
case _: Exception =>
register_GetPool(name) match {
case Success(key) =>
key.Object = obj
Success(obj.GUID.guid)
case Failure(ex) =>
Failure(new Exception(s"trying to register an object but, ${ex.getMessage}"))
}
} else {
register_GetPool(name) match {
case Success(key) =>
key.Object = obj
Success(obj.GUID.guid)
case Failure(ex) =>
Failure(new Exception(s"trying to register an object but, ${ex.getMessage}"))
}
}
}
private def register_CheckNumberAgainstDesiredPool(obj: IdentifiableEntity, name: String, number: Int): Try[Int] = {
val directKey = source.Get(number)
val directKey = source.get(number)
if (directKey.isEmpty || !directKey.get.Object.contains(obj)) {
Failure(new Exception("object already registered, but not to this source"))
} else if (!WhichPool(number).contains(name)) {
@ -347,11 +347,34 @@ class NumberPoolHub(private val source: NumberSource) {
case Success(pool) =>
val number = obj.GUID.guid
pool.Return(number)
source.Return(number)
source.returnNumber(number)
obj.Invalidate()
Success(number)
case Failure(ex) =>
Failure(new Exception(s"can not unregister this object: ${ex.getMessage}"))
unregister_GetMonitorFromObject(obj, ex.getMessage)
}
}
/**
* Unregister a specific object
* by actually finding the object itself, if it exists.
* @param obj an object being unregistered
* @param msg custom error message;
* has a vague default
* @return the number associated with this object
*/
def unregister_GetMonitorFromObject(
obj: IdentifiableEntity,
msg: String = "can not find this object"
): Try[Int] = {
source.get(obj) match {
case Some(key) =>
val number = key.GUID
GetPool(WhichPool(number).get).get.Return(number)
source.returnNumber(number)
Success(number)
case _ =>
Failure(new Exception(s"can not unregister this $obj - $msg"))
}
}
@ -360,7 +383,7 @@ class NumberPoolHub(private val source: NumberSource) {
case Some(name) =>
unregister_GetPool(name)
case None =>
Failure(throw new Exception("can not find a pool for this object"))
Failure(new Exception(s"can not find a pool for this $obj"))
}
}
@ -379,7 +402,7 @@ class NumberPoolHub(private val source: NumberSource) {
* @return the object, if any, previous associated with the number
*/
def unregister(number: Int): Try[Option[IdentifiableEntity]] = {
if (source.Test(number)) {
if (source.test(number)) {
unregister_GetObjectFromSource(number)
} else {
Failure(new Exception(s"can not unregister a number $number that this source does not own"))
@ -387,7 +410,7 @@ class NumberPoolHub(private val source: NumberSource) {
}
private def unregister_GetObjectFromSource(number: Int): Try[Option[IdentifiableEntity]] = {
source.Return(number) match {
source.returnNumber(number) match {
case Some(obj) =>
unregister_ReturnObjectToPool(obj)
case None =>
@ -403,7 +426,7 @@ class NumberPoolHub(private val source: NumberSource) {
obj.Invalidate()
Success(Some(obj))
case Failure(ex) =>
source.Available(number) //undo
source.getAvailable(number) //undo
Failure(new Exception(s"started unregistering, but ${ex.getMessage}"))
}
}
@ -432,11 +455,11 @@ class NumberPoolHub(private val source: NumberSource) {
}
/**
* For accessing the `Return` function of the contained `NumberSource` directly.
* For accessing the `returnNumber` function of the contained `NumberSource` directly.
* @param number the number to return.
* @return any object previously using this number
*/
def latterPartUnregister(number: Int): Option[IdentifiableEntity] = source.Return(number)
def latterPartUnregister(number: Int): Option[IdentifiableEntity] = source.returnNumber(number)
/**
* Determines if the object is registered.<br>
@ -451,7 +474,7 @@ class NumberPoolHub(private val source: NumberSource) {
*/
def isRegistered(obj: IdentifiableEntity): Boolean = {
try {
source.Get(obj.GUID.guid) match {
source.get(obj.GUID.guid) match {
case Some(monitor) =>
monitor.Object.contains(obj)
case None =>
@ -474,7 +497,7 @@ class NumberPoolHub(private val source: NumberSource) {
* @see `isRegistered(IdentifiableEntity)`
*/
def isRegistered(number: Int): Boolean = {
source.Get(number) match {
source.get(number) match {
case Some(monitor) =>
monitor.Policy == AvailabilityPolicy.Leased
case None =>

View file

@ -73,7 +73,7 @@ object NumberPoolActor {
final case class NoNumber(ex: Throwable, id: Option[Any] = None)
/**
* A message to invoke the `Return` functionality of the current `NumberSelector`.
* A message to invoke the `returnNumber` functionality of the current `NumberSelector`.
* @param number the number
*/
final case class ReturnNumber(number: Int, id: Option[Any] = None)

View file

@ -12,9 +12,9 @@ import net.psforever.objects.guid.AvailabilityPolicy
class LoanedKey(private val guid: Int, private val key: Monitor) {
def GUID: Int = guid
def Policy: AvailabilityPolicy.Value = key.Policy
def Policy: AvailabilityPolicy.Value = key.policy
def Object: Option[IdentifiableEntity] = key.Object
def Object: Option[IdentifiableEntity] = key.obj
/**
* na
@ -30,18 +30,18 @@ class LoanedKey(private val guid: Int, private val key: Monitor) {
*/
def Object_=(obj: Option[IdentifiableEntity]): Option[IdentifiableEntity] = {
if (
key.Policy == AvailabilityPolicy.Leased || (key.Policy == AvailabilityPolicy.Restricted && key.Object.isEmpty)
key.policy == AvailabilityPolicy.Leased || (key.policy == AvailabilityPolicy.Restricted && key.obj.isEmpty)
) {
if (key.Object.isDefined) {
key.Object.get.Invalidate()
key.Object = None
if (key.obj.isDefined) {
key.obj.get.Invalidate()
key.obj = None
}
key.Object = obj
key.obj = obj
if (obj.isDefined) {
import net.psforever.types.PlanetSideGUID
obj.get.GUID = PlanetSideGUID(guid)
}
}
key.Object
key.obj
}
}

View file

@ -5,9 +5,7 @@ import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.guid.AvailabilityPolicy
trait Monitor {
def Policy: AvailabilityPolicy.Value
var policy: AvailabilityPolicy.Value
def Object: Option[IdentifiableEntity]
def Object_=(objct: Option[IdentifiableEntity]): Option[IdentifiableEntity]
var obj: Option[IdentifiableEntity]
}

View file

@ -11,8 +11,8 @@ import net.psforever.objects.guid.AvailabilityPolicy
final class SecureKey(private val guid: Int, private val key: Monitor) {
def GUID: Int = guid
def Policy: AvailabilityPolicy.Value = key.Policy
def Policy: AvailabilityPolicy.Value = key.policy
import net.psforever.objects.entity.IdentifiableEntity
def Object: Option[IdentifiableEntity] = key.Object
def Object: Option[IdentifiableEntity] = key.obj
}

View file

@ -46,7 +46,7 @@ abstract class NumberSelector {
* By default, a simple policy for returning numbers has been provided.
* This will not be sufficient for all selection actions that can be implemented so `override` where necessary.
* <br>
* `Return` is under no obligation to leave its parameter `Array` unmodified.
* `returnNumber` is under no obligation to leave its parameter `Array` unmodified.
* In fact, it should modify it by default to provide additional feedback of its process.
* Pass a copy if data mutation is a concern.
* @param number the number to be returned

View file

@ -6,20 +6,7 @@ import net.psforever.objects.guid.AvailabilityPolicy
import net.psforever.objects.guid.key.Monitor
private class Key extends Monitor {
private var policy: AvailabilityPolicy.Value = AvailabilityPolicy.Available
private var obj: Option[IdentifiableEntity] = None
var policy: AvailabilityPolicy.Value = AvailabilityPolicy.Available
def Policy: AvailabilityPolicy.Value = policy
def Policy_=(pol: AvailabilityPolicy.Value): AvailabilityPolicy.Value = {
policy = pol
Policy
}
def Object: Option[IdentifiableEntity] = obj
def Object_=(objct: Option[IdentifiableEntity]): Option[IdentifiableEntity] = {
obj = objct
Object
}
var obj: Option[IdentifiableEntity] = None
}

View file

@ -1,111 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.guid.source
import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
import net.psforever.objects.guid.AvailabilityPolicy
import scala.collection.mutable
/**
* A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn.
* The numbers are considered to be exclusive.<br>
* <br>
* Produce a series of numbers from 0 to a maximum number (inclusive) to be used as globally unique identifiers (GUIDs).
* @param max the highest number to be generated by this source;
* must be a positive integer or zero
* @throws IllegalArgumentException if `max` is less than zero (therefore the count of generated numbers is at most zero)
* @throws java.lang.NegativeArraySizeException if the count of numbers generated due to max is negative
*/
class LimitedNumberSource(max: Int) extends NumberSource {
if (max < 0) {
throw new IllegalArgumentException(s"non-negative integers only, not $max")
}
private val ary: Array[Key] = Array.ofDim[Key](max + 1)
(0 to max).foreach(x => { ary(x) = new Key })
private var allowRestrictions: Boolean = true
def Size: Int = ary.length
def CountAvailable: Int = ary.count(key => key.Policy == AvailabilityPolicy.Available)
def CountUsed: Int = ary.count(key => key.Policy != AvailabilityPolicy.Available)
def Get(number: Int): Option[SecureKey] = {
if (Test(number)) {
Some(new SecureKey(number, ary(number)))
} else {
None
}
}
def Available(number: Int): Option[LoanedKey] = {
var out: Option[LoanedKey] = None
if (Test(number)) {
val key: Key = ary(number)
if (key.Policy == AvailabilityPolicy.Available) {
key.Policy = AvailabilityPolicy.Leased
out = Some(new LoanedKey(number, key))
}
}
out
}
/**
* Consume the number of a `Monitor` and release that number from its previous assignment/use.
* @param number the number
* @return any object previously using this number
*/
def Return(number: Int): Option[IdentifiableEntity] = {
var out: Option[IdentifiableEntity] = None
if (Test(number)) {
val existing: Key = ary(number)
if (existing.Policy == AvailabilityPolicy.Leased) {
out = existing.Object
existing.Policy = AvailabilityPolicy.Available
existing.Object = None
}
}
out
}
/**
* Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used.
* This wrapped `Monitor` can only be assigned once and the number may not be `Return`ed to this source.
* @param number the number
* @return the wrapped `Monitor`
* @throws ArrayIndexOutOfBoundsException if the requested number is above or below the range
*/
def Restrict(number: Int): Option[LoanedKey] = {
if (allowRestrictions && Test(number)) {
val key: Key = ary(number)
key.Policy = AvailabilityPolicy.Restricted
Some(new LoanedKey(number, key))
} else {
None
}
}
def FinalizeRestrictions: List[Int] = {
allowRestrictions = false
ary.zipWithIndex.filter(entry => entry._1.Policy == AvailabilityPolicy.Restricted).map(entry => entry._2).toList
}
def Clear(): List[IdentifiableEntity] = {
val outList: mutable.ListBuffer[IdentifiableEntity] = mutable.ListBuffer[IdentifiableEntity]()
for (x <- ary.indices) {
ary(x).Policy = AvailabilityPolicy.Available
if (ary(x).Object.isDefined) {
outList += ary(x).Object.get
ary(x).Object = None
}
}
outList.toList
}
}
object LimitedNumberSource {
def apply(max: Int): LimitedNumberSource = {
new LimitedNumberSource(max)
}
}

View file

@ -0,0 +1,121 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.guid.source
import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
import net.psforever.objects.guid.AvailabilityPolicy
/**
* A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn.
* Produce a series of numbers from 0 to a maximum number (inclusive) to be used as globally unique identifiers (GUID's).
* @param max the highest number to be generated by this source;
* must be a positive integer or zero
* @throws IllegalArgumentException if `max` is less than zero (therefore the count of generated numbers is at most zero)
*/
class MaxNumberSource(val max: Int) extends NumberSource {
if (max < 0) {
throw new IllegalArgumentException(s"non-negative integers only, not $max")
}
private val ary: Array[Key] = Array.ofDim[Key](max + 1)
(0 to max).foreach(x => { ary(x) = new Key })
private var allowRestrictions: Boolean = true
def size: Int = ary.length
def countAvailable: Int = ary.count(key => key.policy == AvailabilityPolicy.Available)
def countUsed: Int = ary.count(_.policy != AvailabilityPolicy.Available)
def test(number: Int): Boolean = -1 < number && number < size
def get(number: Int): Option[SecureKey] = {
if (test(number)) {
Some(new SecureKey(number, ary(number)))
} else {
None
}
}
def get(obj: IdentifiableEntity) : Option[SecureKey] = {
ary.zipWithIndex.find { case (key, _) =>
key.obj match {
case Some(o) => o eq obj
case _ => false
}
} match {
case Some((key, number)) => Some(new SecureKey(number, key))
case _=> None
}
}
def getAvailable(number: Int): Option[LoanedKey] = {
var out: Option[LoanedKey] = None
if (test(number)) {
val key: Key = ary(number)
if (key.policy == AvailabilityPolicy.Available) {
key.policy = AvailabilityPolicy.Leased
out = Some(new LoanedKey(number, key))
}
}
out
}
/**
* Consume the number of a `Monitor` and release that number from its previous assignment/use.
* @param number the number
* @return any object previously using this number
*/
def returnNumber(number: Int): Option[IdentifiableEntity] = {
var out: Option[IdentifiableEntity] = None
if (test(number)) {
val existing: Key = ary(number)
if (existing.policy == AvailabilityPolicy.Leased) {
out = existing.obj
existing.policy = AvailabilityPolicy.Available
existing.obj = None
}
}
out
}
/**
* Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used.
* This wrapped `Monitor` can only be assigned once and the number may not be `returnNumber`ed to this source.
* @param number the number
* @return the wrapped `Monitor`
* @throws ArrayIndexOutOfBoundsException if the requested number is above or below the range
*/
def restrictNumber(number: Int): Option[LoanedKey] = {
ary.lift(number) match {
case Some(key: Key) if allowRestrictions && key.policy != AvailabilityPolicy.Restricted =>
key.policy = AvailabilityPolicy.Restricted
Some(new LoanedKey(number, key))
case None =>
None
}
}
def finalizeRestrictions: List[Int] = {
allowRestrictions = false
ary.zipWithIndex.filter(entry => entry._1.policy == AvailabilityPolicy.Restricted).map(entry => entry._2).toList
}
def clear(): List[IdentifiableEntity] = {
val leased = ary.filter(_.policy != AvailabilityPolicy.Available)
leased collect { case key if key.obj.isEmpty =>
key.policy = AvailabilityPolicy.Available
}
leased.toList collect { case key if key.obj.nonEmpty =>
key.policy = AvailabilityPolicy.Available
val out = key.obj.get
key.obj = None
out
}
}
}
object MaxNumberSource {
def apply(max: Int): MaxNumberSource = {
new MaxNumberSource(max)
}
}

View file

@ -18,42 +18,51 @@ import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
* The purpose of a `NumberSource` is to help facilitate globally unique identifiers (GUID, pl. GUIDs).
*/
trait NumberSource {
/**
* The maximum number that can be produced by this source.
* @return the max
*/
def max: Int
/**
* The count of numbers allocated to this source.
* @return the count
*/
def Size: Int
def size: Int
/**
* The count of numbers that can still be drawn.
* @return the count
*/
def CountAvailable: Int
def countAvailable: Int
/**
* The count of numbers that can not be drawn.
* @return the count
*/
def CountUsed: Int
def countUsed: Int
/**
* Is this number a member of this number source?
* @param number the number
* @return `true`, if it is a member; `false`, otherwise
*/
def Test(number: Int): Boolean = -1 < number && number < Size
def test(number: Int): Boolean
/**
* Produce an un-modifiable wrapper for the `Monitor` for this number.
* @param number the number
* @return the wrapped `Monitor`
*/
def Get(number: Int): Option[SecureKey]
def get(number: Int): Option[SecureKey]
//def GetAll(list : List[Int]) : List[SecureKey]
//def GetAll(p : Key => Boolean) : List[SecureKey]
/**
* Produce an un-modifiable wrapper for the `Monitor` for this entity,
* if the entity is discovered being represented in this source.
* @param obj the entity
* @return the wrapped `Monitor`
*/
def get(obj: IdentifiableEntity) : Option[SecureKey]
/**
* Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used.
@ -61,15 +70,15 @@ trait NumberSource {
* @param number the number
* @return the wrapped `Monitor`, or `None`
*/
def Available(number: Int): Option[LoanedKey]
def getAvailable(number: Int): Option[LoanedKey]
/**
* Consume a wrapped `Monitor` and release its number from its previous assignment/use.
* @param monitor the `Monitor`
* @return any object previously using this `Monitor`
*/
def Return(monitor: SecureKey): Option[IdentifiableEntity] = {
Return(monitor.GUID)
def returnNumber(monitor: SecureKey): Option[IdentifiableEntity] = {
returnNumber(monitor.GUID)
}
/**
@ -77,8 +86,8 @@ trait NumberSource {
* @param monitor the `Monitor`
* @return any object previously using this `Monitor`
*/
def Return(monitor: LoanedKey): Option[IdentifiableEntity] = {
Return(monitor.GUID)
def returnNumber(monitor: LoanedKey): Option[IdentifiableEntity] = {
returnNumber(monitor.GUID)
}
/**
@ -86,21 +95,21 @@ trait NumberSource {
* @param number the number
* @return any object previously using this number
*/
def Return(number: Int): Option[IdentifiableEntity]
def returnNumber(number: Int): Option[IdentifiableEntity]
/**
* Produce a modifiable wrapper for the `Monitor` for this number, only if the number has not been used.
* This wrapped `Monitor` can only be assigned once and the number may not be `Return`ed to this source.
* This wrapped `Monitor` can only be assigned once and the number may not be `returnNumber`ed to this source.
* @param number the number
* @return the wrapped `Monitor`
*/
def Restrict(number: Int): Option[LoanedKey]
def restrictNumber(number: Int): Option[LoanedKey]
/**
* Numbers from this source may not longer be marked as `Restricted`.
* @return the `List` of all numbers that have been restricted
*/
def FinalizeRestrictions: List[Int]
def finalizeRestrictions: List[Int]
import net.psforever.objects.entity.IdentifiableEntity
@ -110,5 +119,5 @@ trait NumberSource {
* This is the only way to free `Monitors` that are marked as `Restricted`.
* @return a `List` of assignments maintained by all the currently-used number `Monitors`
*/
def Clear(): List[IdentifiableEntity]
def clear(): List[IdentifiableEntity]
}

View file

@ -0,0 +1,112 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.guid.source
import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.guid.AvailabilityPolicy
import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
/**
* A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn.
* Produce a series of numbers from 0 to a maximum number (inclusive) to be used as globally unique identifiers (GUID's).
* @param values the domain of numbers to be used by this source;
* must only be positive integers or zero
* @throws IllegalArgumentException if no numbers are provided
* @throws IllegalArgumentException if any of the numbers provided are negative
*/
class SpecificNumberSource(values: Iterable[Int]) extends NumberSource {
if (values.isEmpty) {
throw new IllegalArgumentException(s"must provide one or more positive integers (or zero)")
}
values.filter(_ < 0) match {
case Nil => ;
case list => throw new IllegalArgumentException(s"non-negative integers only, not ${list.mkString(" ")}")
}
private val ary : Map[Int, Key] = values.map(index => (index, new Key)).toMap
def max : Int = ary.keys.max
def size : Int = ary.size
def countAvailable : Int = ary.values.count(key => key.policy == AvailabilityPolicy.Available)
def countUsed : Int = ary.values.count(_.policy != AvailabilityPolicy.Available)
def test(number : Int) : Boolean = ary.get(number).nonEmpty
def get(number : Int) : Option[SecureKey] = {
ary.get(number) match {
case Some(key) => Some(new SecureKey(number, key))
case _ => None
}
}
def get(obj : IdentifiableEntity) : Option[SecureKey] = {
ary.find { case (_, key) =>
key.obj match {
case Some(o) => o eq obj
case _ => false
}
} match {
case Some((number, key)) => Some(new SecureKey(number, key))
case _ => None
}
}
def getAvailable(number : Int) : Option[LoanedKey] = {
ary.get(number) match {
case Some(key) if key.policy == AvailabilityPolicy.Available =>
key.policy = AvailabilityPolicy.Leased
Some(new LoanedKey(number, key))
case _ =>
None
}
}
def returnNumber(number : Int) : Option[IdentifiableEntity] = {
ary.get(number) match {
case Some(key) if key.policy == AvailabilityPolicy.Leased =>
val out = key.obj
key.policy = AvailabilityPolicy.Available
key.obj = None
out
case _ =>
None
}
}
def restrictNumber(number : Int) : Option[LoanedKey] = {
ary.get(number) match {
case Some(key) if key.policy != AvailabilityPolicy.Restricted =>
key.policy = AvailabilityPolicy.Restricted
Some(new LoanedKey(number, key))
case _ =>
None
}
}
def finalizeRestrictions : List[Int] = {
ary
.filter { case (_, key : Key) => key.policy == AvailabilityPolicy.Restricted }
.keys
.toList
}
def clear(): List[IdentifiableEntity] = {
val leased = ary.values.filter(_.policy != AvailabilityPolicy.Available)
leased collect { case key if key.obj.isEmpty =>
key.policy = AvailabilityPolicy.Available
}
leased.toList collect { case key if key.obj.nonEmpty =>
key.policy = AvailabilityPolicy.Available
val out = key.obj.get
key.obj = None
out
}
}
}
object SpecificNumberSource {
def apply(values: Iterable[Int]): SpecificNumberSource = {
new SpecificNumberSource(values)
}
}

View file

@ -98,94 +98,3 @@ trait Container {
def Collisions(index: Int, width: Int, height: Int): Try[List[InventoryItem]] =
Inventory.CheckCollisionsVar(index, width, height)
}
//object Container {
// type ValidContainer = PlanetSideServerObject with Container
//
// final case class GetMoveItem(where_src : Int, other : ValidContainer, where_other : Int, other_item : Option[Equipment])
//
// final case class GiveMoveItem(cont1 : ValidContainer, where_cont1 : Int, item : Option[Equipment], cont2 : ValidContainer, where_cont2 : Int, other_item : Option[Equipment])
//
//
// final case class TakeMoveItem(source_index : Int, destination : ValidContainer, destination_index : Int)
//
// final case class TakeMoveItem2(item_guid : PlanetSideGUID, destination : ValidContainer, destination_index : Int)
//
// final case class GivingMoveItem(item : Equipment, source : ValidContainer, source_index : Int, destination : ValidContainer, destination_index : Int)
//
// final case class PutMoveItem(item : Equipment, target_index : Int, source : ValidContainer, source_index : Int)
//
// final case class DropMoveItem(item : Equipment, source : ValidContainer, source_index : Int)
//
// final case class ItemMoved(item : Equipment, location : ValidContainer, location_index : Int)
//
// final case class NoMoveItem(source : ValidContainer, source_index : Int)
//}
//
//trait ContainerBehavior {
// this : Actor =>
//
// def ContainableObject : Container.ValidContainer
//
// val containerBehavior : Receive = {
// case Container.GetMoveItem(where_src, destination, where_dest, other_item) =>
// val slot : EquipmentSlot = ContainableObject.Slot(where_src)
// val equipment = slot.Equipment
// slot.Equipment = None
// sender ! Container.GiveMoveItem(ContainableObject, where_src, equipment, destination, where_dest, other_item)
//
// case Container.TakeMoveItem(source_index, destination, destination_index) =>
// val slot : EquipmentSlot = ContainableObject.Slot(source_index)
// val equipment : Option[Equipment] = slot.Equipment
// slot.Equipment = None
// sender ! (equipment match {
// case Some(item) =>
// Container.GivingMoveItem(item, ContainableObject, source_index, destination, destination_index)
// case None =>
// Container.NoMoveItem(ContainableObject, source_index)
// })
//
// case Container.TakeMoveItem2(item_guid, destination, destination_index) =>
// ContainableObject.Find(item_guid) match {
// case Some(source_index) =>
// val slot : EquipmentSlot = ContainableObject.Slot(source_index)
// val equipment : Option[Equipment] = slot.Equipment
// slot.Equipment = None
// sender ! (equipment match {
// case Some(item) =>
// Container.GivingMoveItem(item, ContainableObject, source_index, destination, destination_index)
// case None => ;
// })
//
// case None =>
// sender ! Container.NoMoveItem(ContainableObject, 65535)
// }
//
// case Container.PutMoveItem(item, target_index, source, source_index) =>
// val slot : EquipmentSlot = ContainableObject.Slot(target_index)
// val equipment : Option[Equipment] = slot.Equipment
// if( {
// val tile = item.Definition.Tile
// ContainableObject.Collisions(target_index, tile.Width, tile.Height) match {
// case Success(Nil) => //no item swap
// true
// case Success(_ :: Nil) => //one item to swap
// true
// case Success(_) | Failure(_) =>
// false //abort when too many items at destination or other failure case
// }
// }) {
// slot.Equipment = None
// slot.Equipment = item
// equipment match {
// case Some(swapItem) =>
// sender ! Container.GivingMoveItem(swapItem, ContainableObject, target_index, source, source_index)
// case None => ;
// }
// sender ! Container.ItemMoved(item, ContainableObject, target_index)
// }
// else {
// sender ! Container.DropMoveItem(item, source, source_index)
// }
// }
//}

View file

@ -191,8 +191,7 @@ class GridInventory extends Container {
} else {
val collisions: mutable.Set[InventoryItem] = mutable.Set[InventoryItem]()
items
.map { case (_, item: InventoryItem) => item }
.foreach { item: InventoryItem =>
.foreach { case (_, item: InventoryItem) =>
val actualItemStart: Int = item.start - offset
val itemx: Int = actualItemStart % width
val itemy: Int = actualItemStart / width
@ -542,8 +541,8 @@ class GridInventory extends Container {
//lists with multiple entries represent a group of items that collide
//perform a specific distinct on the list of lists of numeric id overlaps
//THEN map each list of list's item id to an item
val out = testGrid.collect {
case (_, list) if list.size > 1 => list.sortWith(_ < _)
val out = testGrid.values.collect {
case list if list.size > 1 => list.sortWith(_ < _)
}
recursiveRelatedListCollisions(out.iterator, out.toList).map { list => list.map { items } }
}

View file

@ -0,0 +1,132 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.inventory
import net.psforever.objects.Tool
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.selector.RandomSelector
import net.psforever.objects.guid.source.SpecificNumberSource
import net.psforever.types.PlanetSideGUID
import scala.util.{Failure, Success}
/**
* An inventory that contains its own internal unique number system bound by a domain of numbers.
* When equipment is inserted into this inventory,
* the equipment is registered to it, assigned one of its internal unique numbers.
* The equipment must not already be registered to another unique number system for that reason.
* Upon being removed, the removed equipment is unregistered.
* The registration system adds another unspoken layer to `Capacity`
* as it imposes a total object count to the inventory.
* @see `NumberSourceHub`
* @see `RandomSelector`
* @see `SpecificNumberSource`
* @param numbers the numbers used as unique identifiers
*/
class LocallyRegisteredInventory(numbers: Iterable[Int])
extends GridInventory {
private val hub: NumberPoolHub = {
val numHub = new NumberPoolHub(SpecificNumberSource(numbers))
//only one pool composed of all of the numbers; randomized selection
numHub.AddPool("internal", numbers.toList).Selector = new RandomSelector
numHub
}
override def Insert(start : Int, obj : Equipment) : Boolean = {
if(!obj.HasGUID) {
registerEquipment(obj) match {
case true if super.Insert(start, obj) =>
true
case true =>
unregisterEquipment(obj) //the item failed to be inserted; undo the previous registration
false
case _ =>
false
}
}
else {
false
}
}
override def InsertQuickly(start : Int, obj : Equipment) : Boolean = {
if(!obj.HasGUID) {
registerEquipment(obj) match {
case true if super.InsertQuickly(start, obj) =>
true
case true =>
unregisterEquipment(obj) //the item failed to be inserted; undo the previous registration
false
case _ =>
false
}
}
else {
false
}
}
override def Remove(guid : PlanetSideGUID) : Boolean = {
hub(guid) match {
case Some(obj: Equipment) if super.Remove(guid) =>
unregisterEquipment(obj)
case _ =>
false
}
}
override def Remove(index : Int) : Boolean = {
Slot(index).Equipment match {
case Some(obj: Equipment) if super.Remove(obj.GUID) =>
unregisterEquipment(obj)
case _ =>
false
}
}
override def Clear() : List[InventoryItem] = {
val items = super.Clear()
items.foreach { item => unregisterEquipment(item.obj) }
items
}
private def registerEquipment(obj: Equipment): Boolean = {
obj match {
case tool: Tool => registerTool(tool)
case _ => registerObject(obj)
}
}
private def registerTool(obj: Tool): Boolean = {
val parts = obj +: obj.AmmoSlots.map { _.Box }
val tasks = parts.map { part => hub.register(part, "internal") }
if(tasks.exists(o => o.isInstanceOf[Failure[Int]])) {
tasks.zipWithIndex.collect { case (Success(_), index) =>
unregisterEquipment(parts(index))
}
false
} else {
true
}
}
private def registerObject(obj: Equipment): Boolean = {
hub.register(obj, "internal").isSuccess
}
private def unregisterEquipment(obj: Equipment): Boolean = {
obj match {
case tool: Tool => unregisterTool(tool)
case _ => unregisterObject(obj)
}
}
private def unregisterTool(obj: Tool): Boolean = {
val parts = obj +: obj.AmmoSlots.map { _.Box }
parts.map { part => hub.unregister(part) }.forall(o => o.isInstanceOf[Success[Int]])
}
private def unregisterObject(obj: Equipment): Boolean = {
hub.unregister(obj).isSuccess
}
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.locker
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.definition.EquipmentDefinition
import net.psforever.objects.inventory.{Container, GridInventory}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.types.PlanetSideEmpire
/**
* The companion of a `Locker` that is carried with a player
* masquerading as their sixth `EquipmentSlot` object and a sub-inventory item.
* The inventory of this object is accessed indirectly using a game world `Locker` object (`mb_locker`) as a proxy.
* The `Player` class refers to it as the "fifth slot".
*/
class LockerContainer(inventory: GridInventory)
extends PlanetSideServerObject
with Container {
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private val inv: GridInventory = inventory
def Faction: PlanetSideEmpire.Value = faction
override def Faction_=(fact: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
faction = fact
Faction
}
def Inventory: GridInventory = inv
def VisibleSlots: Set[Int] = Set.empty[Int]
def Definition: EquipmentDefinition = GlobalDefinitions.locker_container
}
object LockerContainer {
/**
* Overloaded constructor for the standard Infantry locker container of size 30x20.
* @return a `LockerContainer` object
*/
def apply(): LockerContainer = {
new LockerContainer(GridInventory(30, 20))
}
}

View file

@ -1,69 +1,24 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
// Copyright (c) 2020 PSForever
package net.psforever.objects.locker
import akka.actor.Actor
import net.psforever.objects.definition.EquipmentDefinition
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.{Container, GridInventory}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.packet.game.{ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectDetachMessage}
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.packet.game.{ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectDetachMessage}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3}
/**
* The companion of a `Locker` that is carried with a player
* masquerading as their sixth `EquipmentSlot` object and a sub-inventory item.
* The `Player` class refers to it as the "fifth slot" as its permanent slot number is encoded as `0x85`.
* The inventory of this object is accessed using a game world `Locker` object (`mb_locker`).
* A control agency mainly for manipulating the equipment stowed by a player in a `LockerContainer`
* and reporting back to a specific xchannel in the event system about these changes.
* @param locker the governed player-facing locker component
* @param toChannel the channel to which to publish events, typically the owning player's name
*/
class LockerContainer extends PlanetSideServerObject with Container {
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private val inventory = GridInventory(30, 20)
def Faction: PlanetSideEmpire.Value = faction
override def Faction_=(fact: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
faction = fact
Faction
}
def Inventory: GridInventory = inventory
def VisibleSlots: Set[Int] = Set.empty[Int]
def Definition: EquipmentDefinition = GlobalDefinitions.locker_container
}
object LockerContainer {
def apply(): LockerContainer = {
new LockerContainer()
}
}
class LockerEquipment(locker: LockerContainer) extends Equipment with Container {
private val obj = locker
override def GUID: PlanetSideGUID = obj.GUID
override def GUID_=(guid: PlanetSideGUID): PlanetSideGUID = obj.GUID_=(guid)
override def HasGUID: Boolean = obj.HasGUID
override def Invalidate(): Unit = obj.Invalidate()
override def Faction: PlanetSideEmpire.Value = obj.Faction
def Inventory: GridInventory = obj.Inventory
def VisibleSlots: Set[Int] = Set.empty[Int]
def Definition: EquipmentDefinition = obj.Definition
}
class LockerContainerControl(locker: LockerContainer, toChannel: String) extends Actor with ContainableBehavior {
class LockerContainerControl(locker: LockerContainer, toChannel: String)
extends Actor
with ContainableBehavior {
def ContainerObject = locker
def receive: Receive =

View file

@ -0,0 +1,41 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.locker
import net.psforever.objects.definition.EquipmentDefinition
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.{Container, GridInventory}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
/**
* A wrapper class that allows a player-facing locker component
* to be treated standard `Equipment` object in the player's fifth (sixth) slot.
* (The opposite is not true - the equipment does not get treated as a locker component.)
* During packet conversion and registration and general access in terms of holsters or "equipment slots",
* the component may be be treated the same as other existing objects at the same level.
* The entity's ability to be utilized like an inventory-stowable entity is not compromised.
* @see `EquipmentSlot`
* @see `IdentifiableEntity`
* @see `LockerContainer`
* @param locker the player-facing locker
*/
class LockerEquipment(locker: LockerContainer)
extends Equipment
with Container {
private val obj = locker
override def GUID: PlanetSideGUID = obj.GUID
override def GUID_=(guid: PlanetSideGUID): PlanetSideGUID = obj.GUID_=(guid)
override def HasGUID: Boolean = obj.HasGUID
override def Invalidate(): Unit = obj.Invalidate()
override def Faction: PlanetSideEmpire.Value = obj.Faction
def Inventory: GridInventory = obj.Inventory
def VisibleSlots: Set[Int] = Set.empty[Int]
def Definition: EquipmentDefinition = obj.Definition
}

View file

@ -12,7 +12,7 @@ import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.actor.UniqueNumberSystem
import net.psforever.objects.guid.key.LoanedKey
import net.psforever.objects.guid.selector.RandomSelector
import net.psforever.objects.guid.source.LimitedNumberSource
import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.painbox.{Painbox, PainboxDefinition}
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
@ -73,7 +73,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
private var accessor: ActorRef = ActorRef.noSender
/** The basic support structure for the globally unique number system used by this `Zone`. */
private var guid: NumberPoolHub = new NumberPoolHub(new LimitedNumberSource(65536))
private var guid: NumberPoolHub = new NumberPoolHub(new MaxNumberSource(65536))
/** A synchronized `List` of items (`Equipment`) dropped by players on the ground and can be collected again. */
private val equipmentOnGround: ListBuffer[Equipment] = ListBuffer[Equipment]()
@ -322,6 +322,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
guid.AddPool("f", (30001 to 35000).toList).Selector = new RandomSelector
guid.AddPool("g", (35001 until 40100).toList).Selector = new RandomSelector
guid.AddPool("projectiles", (Projectile.baseUID until Projectile.rangeUID).toList)
guid.AddPool("locker-contents", (40150 until 40450).toList).Selector = new RandomSelector
//TODO disabled temporarily to lighten load times
//guid.AddPool("h", (40150 to 45000).toList).Selector = new RandomSelector
//guid.AddPool("i", (45001 to 50000).toList).Selector = new RandomSelector
@ -358,12 +359,12 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
building match {
case warpGate: WarpGate =>
warpGate.Faction == faction || warpGate.Faction == PlanetSideEmpire.NEUTRAL || warpGate.Broadcast
case building =>
case _ =>
building.Faction == faction
}
}
.map {
case (building, spawns) =>
case (building, spawns: List[SpawnPoint]) =>
(building, spawns.filter(!_.Offline))
}
.concat(

View file

@ -364,13 +364,12 @@ class PersistenceMonitor(name: String, squadService: ActorRef, taskResolver: Act
//player has released
//our last body was turned into a corpse; just the avatar remains
//TODO perform any last minute saving now ...
AvatarLogout(avatar)
inZone.GUID(avatar.vehicle) match {
case Some(obj: Vehicle) if obj.OwnerName.contains(avatar.name) =>
obj.Actor ! Vehicle.Ownership(None)
case _ => ;
}
taskResolver.tell(GUIDTask.UnregisterLocker(avatar.locker)(inZone.GUID), context.parent)
AvatarLogout(avatar)
case _ =>
//user stalled during initial session, or was caught in between zone transfer
@ -386,7 +385,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef, taskResolver: Act
* @see `Avatar`
* @see `AvatarAction.ObjectDelete`
* @see `AvatarServiceMessage`
* @see `GUIDTask.UnregisterAvatar`
* @see `GUIDTask.UnregisterPlayer`
* @see `Player`
* @see `Zone.AvatarEvents`
* @see `Zone.Population.Release`
@ -405,8 +404,8 @@ class PersistenceMonitor(name: String, squadService: ActorRef, taskResolver: Act
}
inZone.Population.tell(Zone.Population.Release(avatar), parent)
inZone.AvatarEvents.tell(AvatarServiceMessage(inZone.id, AvatarAction.ObjectDelete(pguid, pguid)), parent)
taskResolver.tell(GUIDTask.UnregisterPlayer(player)(inZone.GUID), parent)
AvatarLogout(avatar)
taskResolver.tell(GUIDTask.UnregisterAvatar(player)(inZone.GUID), parent)
}
/**
@ -424,6 +423,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef, taskResolver: Act
squadService.tell(Service.Leave(Some(avatar.id.toString)), context.parent)
Deployables.Disown(inZone, avatar, context.parent)
inZone.Population.tell(Zone.Population.Leave(avatar), context.parent)
taskResolver.tell(GUIDTask.UnregisterObjectTask(avatar.locker)(inZone.GUID), context.parent)
log.info(s"logout of ${avatar.name}")
}
}

View file

@ -2,21 +2,14 @@ package net.psforever.zones
import java.io.FileNotFoundException
import net.psforever.objects.serverobject.terminals.{
CaptureTerminal,
CaptureTerminalDefinition,
ProximityTerminal,
ProximityTerminalDefinition,
Terminal,
TerminalDefinition
}
import net.psforever.objects.serverobject.terminals.{CaptureTerminal, CaptureTerminalDefinition, ProximityTerminal, ProximityTerminalDefinition, Terminal, TerminalDefinition}
import net.psforever.objects.serverobject.mblocker.Locker
import java.util.concurrent.atomic.AtomicInteger
import akka.actor.ActorContext
import io.circe._
import io.circe.parser._
import net.psforever.objects.{GlobalDefinitions, LocalProjectile}
import net.psforever.objects.{GlobalDefinitions, LocalLockerItem, LocalProjectile}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.definition.BasicDefinition
import net.psforever.objects.serverobject.doors.Door
@ -25,18 +18,13 @@ import net.psforever.objects.serverobject.locks.IFFLock
import net.psforever.objects.serverobject.pad.{VehicleSpawnPad, VehicleSpawnPadDefinition}
import net.psforever.objects.serverobject.painbox.{Painbox, PainboxDefinition}
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{
Building,
BuildingDefinition,
FoundationBuilder,
StructureType,
WarpGate
}
import net.psforever.objects.serverobject.structures.{Building, BuildingDefinition, FoundationBuilder, StructureType, WarpGate}
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition}
import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.DefinitionUtil
import scala.io.Source
import scala.collection.parallel.CollectionConverters._
@ -281,6 +269,9 @@ object Zones {
(Projectile.baseUID until Projectile.rangeUID) foreach {
zoneMap.addLocalObject(_, LocalProjectile.Constructor)
}
40150 until 40450 foreach {
zoneMap.addLocalObject(_, LocalLockerItem.Constructor)
}
lattice.asObject.get(info.value).foreach { obj =>
obj.asArray.get.foreach { entry =>