diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 1c2c16c25..73d67cd58 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -44,6 +44,7 @@ import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.vehicles._ import net.psforever.objects.vital._ import net.psforever.objects.vital.base._ +import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} import net.psforever.objects.vital.etc.ExplodingEntityReason import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.projectile.ProjectileReason @@ -206,7 +207,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var nextSpawnPoint: Option[SpawnPoint] = None var setupAvatarFunc: () => Unit = AvatarCreate var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally - var persist: () => Unit = NoPersistence + var persistFunc: () => Unit = NoPersistence + var persist: () => Unit = UpdatePersistenceOnly var specialItemSlotGuid: Option[PlanetSideGUID] = None // If a special item (e.g. LLU) has been attached to the player the GUID should be stored here, or cleared when dropped, since the drop hotkey doesn't send the GUID of the object to be dropped. @@ -275,6 +277,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var setAvatar: Boolean = false var turnCounterFunc: PlanetSideGUID => Unit = TurnCounterDuringInterim var waypointCooldown: Long = 0L + var heightLast: Float = 0f + var heightTrend: Boolean = false //up = true, down = false + var heightHistory: Float = 0f + val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap() var clientKeepAlive: Cancellable = Default.Cancellable var progressBarUpdate: Cancellable = Default.Cancellable @@ -1172,6 +1178,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case NewPlayerLoaded(tplayer) => //new zone log.info(s"${tplayer.Name} has spawned into ${session.zone.id}") + persist = UpdatePersistenceAndRefs tplayer.avatar = avatar session = session.copy(player = tplayer) avatarActor ! AvatarActor.CreateImplants() @@ -1379,7 +1386,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case PlayerToken.LoginInfo(name, Zone.Nowhere, _) => log.info(s"LoginInfo: player $name is considered a new character") //TODO poll the database for saved zone and coordinates? - persist = UpdatePersistence(sender()) + persistFunc = UpdatePersistence(sender()) deadState = DeadState.RespawnTime session = session.copy(player = new Player(avatar)) @@ -1395,7 +1402,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case PlayerToken.LoginInfo(playerName, inZone, pos) => log.info(s"LoginInfo: player $playerName is already logged in zone ${inZone.id}; rejoining that character") - persist = UpdatePersistence(sender()) + persistFunc = UpdatePersistence(sender()) //tell the old WorldSessionActor to kill itself by using its own subscriptions against itself inZone.AvatarEvents ! AvatarServiceMessage(playerName, AvatarAction.TeardownConnection()) //find and reload previous player @@ -1477,18 +1484,36 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /** * Update this player avatar for persistence. - * @param persistRef reference to the persistence monitor + * Set to `persist` initially. */ - def UpdatePersistence(persistRef: ActorRef)(): Unit = { - persistRef ! AccountPersistenceService.Update(player.Name, continent, player.Position) + def UpdatePersistenceOnly(): Unit = { + persistFunc() + } + + /** + * Update this player avatar for persistence. + * Set to `persist` when (new) player is loaded. + */ + def UpdatePersistenceAndRefs(): Unit = { + persistFunc() updateOldRefsMap() } /** * Do not update this player avatar for persistence. + * Set to `persistFunc` initially. */ def NoPersistence(): Unit = {} + /** + * Update this player avatar for persistence. + * Set this to `persistFunc` when persistence is ready. + * @param persistRef reference to the persistence monitor + */ + def UpdatePersistence(persistRef: ActorRef)(): Unit = { + persistRef ! AccountPersistenceService.Update(player.Name, continent, player.Position) + } + /** * A zoning message was received. * That doesn't matter. @@ -1585,11 +1610,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con CancelAllProximityUnits() //droppod action val droppod = Vehicle(GlobalDefinitions.droppod) + droppod.GUID = PlanetSideGUID(0) //droppod is not registered, we must jury-rig this droppod.Faction = player.Faction droppod.Position = spawnPosition.xy + Vector3.z(1024) droppod.Orientation = Vector3.z(180) //you always seems to land looking south; don't know why droppod.Seats(0).mount(player) - droppod.GUID = PlanetSideGUID(0) //droppod is not registered, we must jury-rig this droppod.Invalidate() //now, we must short-circuit the jury-rig interstellarFerry = Some(droppod) //leverage vehicle gating player.Position = droppod.Position @@ -1642,7 +1667,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def HandleAvatarServiceResponse(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = { val tplayer_guid = - if (player.HasGUID) player.GUID + if (player != null && player.HasGUID) player.GUID else PlanetSideGUID(0) reply match { case AvatarResponse.TeardownConnection() => @@ -2525,12 +2550,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.Actor ! Vehicle.Deconstruct() case Mountable.CanDismount(obj: Vehicle, seat_num, _) => - log.info( - s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name} from seat #$seat_num" - ) val player_guid: PlanetSideGUID = tplayer.GUID if (player_guid == player.GUID) { //disembarking self + log.info( + s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name} from seat #$seat_num" + ) ConditionalDriverVehicleControl(obj) UnaccessContainer(obj) DismountAction(tplayer, obj, seat_num) @@ -2742,12 +2767,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //but always seems to return 4 if user is kicked by mount permissions changing sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) if (tplayer_guid == guid) { - log.info(s"{${player.Name} has been kicked from ${player.Sex.possessive} ride!") - continent.GUID(vehicle_guid) match { + val typeOfRide = continent.GUID(vehicle_guid) match { case Some(obj: Vehicle) => UnaccessContainer(obj) - case _ => ; + s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}" + case _ => + s"${player.Sex.possessive} ride" } + log.info(s"${player.Name} has been kicked from $typeOfRide!") } case VehicleResponse.InventoryState2(obj_guid, parent_guid, value) => @@ -2969,7 +2996,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con cargo: Vehicle, mountPoint: Int ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { - val msgs @ (attachMessage, mountPointStatusMessage) = CargoBehavior.CargoMountMessages(carrier, cargo, mountPoint) + val msgs @ (attachMessage, mountPointStatusMessage) = CarrierBehavior.CargoMountMessages(carrier, cargo, mountPoint) CargoMountMessagesForUs(attachMessage, mountPointStatusMessage) msgs } @@ -3336,8 +3363,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match { case (Some(cargo: Vehicle), Some(carrier: Vehicle)) => carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match { - case Some((mountPoint, _)) => //try begin the mount process - cargo.Actor ! CargoBehavior.CheckCargoMounting(carrier_guid, mountPoint, 0) + case Some((mountPoint, _)) => + cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint) case _ => log.warn( s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold" @@ -3350,17 +3377,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _ => ; } - case msg @ DismountVehicleCargoMsg(player_guid, cargo_guid, bailed, requestedByPassenger, kicked) => + case msg @ DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) => log.debug(s"DismountVehicleCargoMsg: $msg") - //when kicked by carrier driver, player_guid will be PlanetSideGUID(0) - //when exiting of the cargo vehicle driver's own accord, player_guid will be the cargo vehicle driver continent.GUID(cargo_guid) match { - case Some(cargo: Vehicle) if !requestedByPassenger => - continent.GUID(cargo.MountedIn) match { - case Some(carrier: Vehicle) => - CargoBehavior.HandleVehicleCargoDismount(continent, cargo_guid, bailed, requestedByPassenger, kicked) - case _ => ; - } + case Some(cargo: Vehicle) => + cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked) case _ => ; } @@ -3633,7 +3654,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case vehicle if vehicle.CargoHolds.nonEmpty => vehicle.CargoHolds.collect { case (_index, hold: Cargo) if hold.isOccupied => - CargoBehavior.CargoMountBehaviorForAll( + CarrierBehavior.CargoMountBehaviorForAll( vehicle, hold.occupant.get, _index @@ -3779,6 +3800,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (isMovingPlus) { CancelZoningProcessWithDescriptiveReason("cancel_motion") } + fallHeightTracker(pos.z) // if (is_crouching && !player.Crouching) { // //dev stuff goes here // } @@ -3909,10 +3931,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //we're driving the vehicle persist() turnCounterFunc(player.GUID) + fallHeightTracker(pos.z) if (obj.MountedIn.isEmpty) { updateBlockMap(obj, continent, pos) } - val seat = obj.Seats(0) player.Position = pos //convenient if (obj.WeaponControlledFromSeat(0).isEmpty) { player.Orientation = Vector3.z(ang.z) //convenient @@ -3969,10 +3991,38 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con KickedByAdministration() } - case msg @ VehicleSubStateMessage(vehicle_guid, player_guid, vehicle_pos, vehicle_ang, vel, unk1, unk2) => - log.debug( - s"VehicleSubState: $vehicle_guid, ${player.Name}_guid, $vehicle_pos, $vehicle_ang, $vel, $unk1, $unk2" - ) + case msg @ VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, unk2) => + //log.info(s"msg") + ValidObject(vehicle_guid) match { + case Some(obj: Vehicle) => + obj.Position = pos + obj.Orientation = ang + obj.Velocity = vel + updateBlockMap(obj, continent, pos) + obj.zoneInteractions() + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.VehicleState( + player.GUID, + vehicle_guid, + unk1, + pos, + ang, + obj.Velocity, + obj.Flying, + 0, + 0, + 15, + false, + obj.Cloaked + ) + ) + + case _ => + log.warn( + s"VehicleSubState: ${player.Name} should not be dispatching this kind of packet for vehicle #${vehicle_guid.guid}" + ) + } case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) => val index = projectile_guid.guid - Projectile.baseUID @@ -5447,7 +5497,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(obj: Mountable) => obj.PassengerInSeat(player) match { case Some(seat_num) => - obj.Actor ! Mountable.TryDismount(player, seat_num) + obj.Actor ! Mountable.TryDismount(player, seat_num, bailType) if (interstellarFerry.isDefined) { //short-circuit the temporary channel for transferring between zones, the player is no longer doing that //see above in VehicleResponse.TransferPassenger case @@ -5489,7 +5539,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case (Some(obj: Mountable), Some(tplayer: Player)) => obj.PassengerInSeat(tplayer) match { case Some(seat_num) => - obj.Actor ! Mountable.TryDismount(tplayer, seat_num) + obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType) case None => dismountWarning( s"DismountVehicleMsg: can not find where other player ${player.Name}_guid is seated in mountable $obj_guid" @@ -5552,8 +5602,111 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info)) } - case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) => - log.info(s"${player.Name} would be in intense and excruciating pain right now if collision worked") + case msg @ GenericCollisionMsg(ctype, p, php, ppos, pv, t, thp, tpos, tv, u1, u2, u3) => + //log.info(s"$msg") + val fallHeight = { + if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) { + if (heightTrend) { + val fall = heightLast - heightHistory + heightHistory = heightLast + fall + } + else { + val fall = heightHistory - heightLast + heightLast = heightHistory + fall + } + } else { + 0f + } + } + val (target1, target2, bailProtectStatus, velocity) = (ctype, ValidObject(p)) match { + case (CollisionIs.OfInfantry, out @ Some(user: Player)) + if user == player => + val bailStatus = session.flying || player.spectator || session.speed > 1f || player.BailProtection + player.BailProtection = false + val v = if (player.avatar.implants.exists { + case Some(implant) => implant.definition.implantType == ImplantType.Surge && implant.active + case _ => false + }) { + Vector3.Zero + } else { + pv + } + (out, None, bailStatus, v) + case (CollisionIs.OfGroundVehicle, out @ Some(v: Vehicle)) + if v.Seats(0).occupant.contains(player) => + val bailStatus = v.BailProtection + v.BailProtection = false + (out, ValidObject(t), bailStatus, pv) + case (CollisionIs.OfAircraft, out @ Some(v: Vehicle)) + if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => + (out, ValidObject(t), false, pv) + case _ => + (None, None, false, Vector3.Zero) + } + val curr = System.currentTimeMillis() + (target1, t, target2) match { + case (None, _, _) => ; + + case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) => + if (collisionHistory.get(us.Actor) match { + case Some(lastCollision) if curr - lastCollision <= 1000L => + false + case _ => + collisionHistory.put(us.Actor, curr) + true + }) { + if (!bailProtectStatus) { + HandleDealingDamage( + us, + DamageInteraction( + SourceEntry(us), + CollisionReason(velocity, fallHeight, us.DamageModel), + ppos + ) + ) + } + } + + case ( + Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _, + Some(victim: PlanetSideServerObject with Vitality with FactionAffinity) + ) => + if (collisionHistory.get(victim.Actor) match { + case Some(lastCollision) if curr - lastCollision <= 1000L => + false + case _ => + collisionHistory.put(victim.Actor, curr) + true + }) { + val usSource = SourceEntry(us) + val victimSource = SourceEntry(victim) + //we take damage from the collision + if (!bailProtectStatus) { + HandleDealingDamage( + us, + DamageInteraction( + usSource, + CollisionWithReason(CollisionReason(velocity - tv, fallHeight, us.DamageModel), victimSource), + ppos + ) + ) + } + //get dealt damage from our own collision (no protection) + collisionHistory.put(us.Actor, curr) + HandleDealingDamage( + victim, + DamageInteraction( + victimSource, + CollisionWithReason(CollisionReason(tv - velocity, 0, victim.DamageModel), usSource), + tpos + ) + ) + } + + case _ => ; + } case msg @ BugReportMessage( version_major, @@ -6760,6 +6913,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con progressBarValue = None lastTerminalOrderFulfillment = true kitToBeUsed = None + collisionHistory.clear() accessedContainer match { case Some(v: Vehicle) => val vguid = v.GUID @@ -6863,7 +7017,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) carrierInfo match { case (Some(carrier), Some((index, _))) => - CargoBehavior.CargoMountBehaviorForOthers(carrier, vehicle, index, player.GUID) + CarrierBehavior.CargoMountBehaviorForOthers(carrier, vehicle, index, player.GUID) case _ => vehicle.MountedIn = None } @@ -7725,10 +7879,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def DismountAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { val player_guid: PlanetSideGUID = tplayer.GUID keepAliveFunc = NormalKeepAlive - sendResponse(DismountVehicleMsg(player_guid, BailType.Normal, wasKickedByDriver = false)) + val bailType = if (tplayer.BailProtection) { + BailType.Bailed + } else { + BailType.Normal + } + sendResponse(DismountVehicleMsg(player_guid, bailType, wasKickedByDriver = false)) continent.VehicleEvents ! VehicleServiceMessage( continent.id, - VehicleAction.DismountVehicle(player_guid, BailType.Normal, false) + VehicleAction.DismountVehicle(player_guid, bailType, false) ) } @@ -7748,18 +7907,41 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val func = data.calculate() target match { case obj: Player if obj.CanDamage && obj.Actor != Default.Actor => - log.info(s"${player.Name} is attacking ${obj.Name}") + if (obj.CharId != player.CharId) { + log.info(s"${player.Name} is attacking ${obj.Name}") + } else { + log.info(s"${player.Name} hurt ${player.Sex.pronounObject}self") + } // auto kick players damaging spectators if (obj.spectator && obj != player) { AdministrativeKick(player) } else { obj.Actor ! Vitality.Damage(func) } + case obj: Vehicle if obj.CanDamage => - log.info(s"${player.Name} is attacking ${obj.OwnerName.getOrElse("someone")}'s ${obj.Definition.Name}") + val name = player.Name + val ownerName = obj.OwnerName.getOrElse("someone") + if (ownerName.equals(name)) { + log.info(s"$name is damaging ${player.Sex.possessive} own ${obj.Definition.Name}") + } else { + log.info(s"$name is attacking $ownerName's ${obj.Definition.Name}") + } obj.Actor ! Vitality.Damage(func) - case obj: Amenity if obj.CanDamage => obj.Actor ! Vitality.Damage(func) - case obj: Deployable if obj.CanDamage => obj.Actor ! Vitality.Damage(func) + + case obj: Amenity if obj.CanDamage => + obj.Actor ! Vitality.Damage(func) + + case obj: Deployable if obj.CanDamage => + val name = player.Name + val ownerName = obj.OwnerName.getOrElse("someone") + if (ownerName.equals(name)) { + log.info(s"$name is damaging ${player.Sex.possessive} own ${obj.Definition.Name}") + } else { + log.info(s"$name is attacking $ownerName's ${obj.Definition.Name}") + } + obj.Actor ! Vitality.Damage(func) + case _ => ; } } @@ -8197,7 +8379,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.warn( s"LoadZoneInVehicleAsDriver: ${player.Name} must eject cargo in hold $index; vehicle is missing driver" ) - CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true) + cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false) case entry => val cargo = vehicle.CargoHolds(entry.mount).occupant.get continent.VehicleEvents ! VehicleServiceMessage( @@ -9139,26 +9321,28 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]() def updateOldRefsMap(): Unit = { - oldRefsMap.addAll( - (continent.GUID(player.VehicleSeated) match { - case Some(v: Vehicle) => - v.Weapons.toList.collect { - case (_, slot: EquipmentSlot) if slot.Equipment.nonEmpty => updateOldRefsMap(slot.Equipment.get) - }.flatten ++ - updateOldRefsMap(v.Inventory) - case _ => - Map.empty[PlanetSideGUID, String] - }) ++ - (accessedContainer match { - case Some(cont) => updateOldRefsMap(cont.Inventory) - case None => Map.empty[PlanetSideGUID, String] - }) ++ - player.Holsters().toList.collect { - case slot if slot.Equipment.nonEmpty => updateOldRefsMap(slot.Equipment.get) - }.flatten ++ - updateOldRefsMap(player.Inventory) ++ - updateOldRefsMap(player.avatar.locker.Inventory) - ) + if(player.HasGUID) { + oldRefsMap.addAll( + (continent.GUID(player.VehicleSeated) match { + case Some(v : Vehicle) => + v.Weapons.toList.collect { + case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => updateOldRefsMap(slot.Equipment.get) + }.flatten ++ + updateOldRefsMap(v.Inventory) + case _ => + Map.empty[PlanetSideGUID, String] + }) ++ + (accessedContainer match { + case Some(cont) => updateOldRefsMap(cont.Inventory) + case None => Map.empty[PlanetSideGUID, String] + }) ++ + player.Holsters().toList.collect { + case slot if slot.Equipment.nonEmpty => updateOldRefsMap(slot.Equipment.get) + }.flatten ++ + updateOldRefsMap(player.Inventory) ++ + updateOldRefsMap(player.avatar.locker.Inventory) + ) + } } def updateOldRefsMap(inventory: net.psforever.objects.inventory.GridInventory): IterableOnce[(PlanetSideGUID, String)] = { @@ -9179,6 +9363,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } + def fallHeightTracker(zHeight: Float): Unit = { + if ((heightTrend && heightLast - zHeight >= 0.5f) || + (!heightTrend && zHeight - heightLast >= 0.5f)) { + heightTrend = !heightTrend +// if (heightTrend) { +// GetMountableAndSeat(None, player, continent) match { +// case (Some(v: Vehicle), _) => v.BailProtection = false +// case _ => player.BailProtection = false +// } +// } + heightHistory = zHeight + } + heightLast = zHeight + } + def failWithError(error: String) = { log.error(error) middlewareActor ! MiddlewareActor.Teardown() diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala index 149fb0e48..3dec97166 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -137,7 +137,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone) zone.blockMap.addTo(target, toPosition) case UpdateBlockMap(target, toPosition) => - zone.blockMap.move(target, toPosition) + target.updateBlockMapEntry(toPosition) case RemoveFromBlockMap(target) => zone.blockMap.removeFrom(target) diff --git a/src/main/scala/net/psforever/objects/Deployables.scala b/src/main/scala/net/psforever/objects/Deployables.scala index 8e5b9d333..596a96fb0 100644 --- a/src/main/scala/net/psforever/objects/Deployables.scala +++ b/src/main/scala/net/psforever/objects/Deployables.scala @@ -146,19 +146,30 @@ object Deployables { * If the default ammunition mode for the `ConstructionTool` is not supported by the given certifications, * find a suitable ammunition mode and switch to it internally. * No special complaint is raised if the `ConstructionItem` itself is completely unsupported. + * The search function will explore every ammo option for every fire mode option + * and will stop when it finds either a valid option or when arrives back at the original fire mode. * @param certs the certification baseline being compared against * @param obj the `ConstructionItem` entity - * @return `true`, if the ammunition mode of the item has been changed; + * @return `true`, if the firemode and ammunition mode of the item is valid; * `false`, otherwise */ - def initializeConstructionAmmoMode( - certs: Set[Certification], - obj: ConstructionItem - ): Boolean = { + def initializeConstructionItem( + certs: Set[Certification], + obj: ConstructionItem + ): Boolean = { + val initialFireModeIndex = obj.FireModeIndex if (!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions)) { - Deployables.performConstructionItemAmmoChange(certs, obj, obj.AmmoTypeIndex) + while (!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions) && + !Deployables.performConstructionItemAmmoChange(certs, obj, obj.AmmoTypeIndex) && + { + obj.NextFireMode + initialFireModeIndex != obj.FireModeIndex + }) { + /* change in fire mode occurs in conditional */ + } + Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions) } else { - false + true } } diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index f98bef474..889582ca3 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -31,22 +31,28 @@ import net.psforever.objects.vital.damage._ import net.psforever.objects.vital.etc.ExplodingRadialDegrade import net.psforever.objects.vital.projectile._ import net.psforever.objects.vital.prop.DamageWithPosition -import net.psforever.objects.vital.{ComplexDeployableResolutions, MaxResolutions, SimpleResolutions} +import net.psforever.objects.vital._ import net.psforever.types.{ExoSuitType, ImplantType, PlanetSideEmpire, Vector3} import net.psforever.types._ import net.psforever.objects.serverobject.llu.{CaptureFlagDefinition, CaptureFlagSocketDefinition} +import net.psforever.objects.vital.collision.TrapCollisionDamageMultiplier import scala.collection.mutable import scala.concurrent.duration._ object GlobalDefinitions { - // Characters + /* + characters + */ val avatar = new AvatarDefinition(121) avatar.MaxHealth = 100 avatar.Damageable = true avatar.DrownAtMaxDepth = true avatar.MaxDepth = 1.609375f //Male, standing, not MAX avatar.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L) + avatar.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB + avatar.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB + avatar.maxForwardSpeed = 27f //not in the ADB; running speed /* exo-suits */ @@ -1704,6 +1710,8 @@ object GlobalDefinitions { Standard.ResistanceDirectHit = 4 Standard.ResistanceSplash = 15 Standard.ResistanceAggravated = 8 + Standard.collision.forceFactor = 1.5f + Standard.collision.massFactor = 2f Agile.Name = "lite_armor" Agile.Descriptor = "agile" @@ -1717,6 +1725,8 @@ object GlobalDefinitions { Agile.ResistanceDirectHit = 6 Agile.ResistanceSplash = 25 Agile.ResistanceAggravated = 10 + Agile.collision.forceFactor = 1.5f + Agile.collision.massFactor = 2f Reinforced.Name = "med_armor" Reinforced.Descriptor = "reinforced" @@ -1732,6 +1742,8 @@ object GlobalDefinitions { Reinforced.ResistanceDirectHit = 10 Reinforced.ResistanceSplash = 35 Reinforced.ResistanceAggravated = 12 + Reinforced.collision.forceFactor = 2f + Reinforced.collision.massFactor = 3f Infiltration.Name = "infiltration_suit" Infiltration.Permissions = List(Certification.InfiltrationSuit) @@ -1752,6 +1764,8 @@ object GlobalDefinitions { max.ResistanceDirectHit = 6 max.ResistanceSplash = 35 max.ResistanceAggravated = 10 + max.collision.forceFactor = 4f + max.collision.massFactor = 10f max.DamageUsing = DamageCalculations.AgainstMaxSuit max.Model = MaxResolutions.calculate } @@ -5652,10 +5666,17 @@ object GlobalDefinitions { * Initialize `VehicleDefinition` globals. */ private def init_vehicles(): Unit = { + init_ground_vehicles() + init_flight_vehicles() + } + + /** + * Initialize land-based `VehicleDefinition` globals. + */ + private def init_ground_vehicles(): Unit = { val atvForm = GeometryForm.representByCylinder(radius = 1.1797f, height = 1.1875f) _ val delivererForm = GeometryForm.representByCylinder(radius = 2.46095f, height = 2.40626f) _ //TODO hexahedron val apcForm = GeometryForm.representByCylinder(radius = 4.6211f, height = 3.90626f) _ //TODO hexahedron - val liberatorForm = GeometryForm.representByCylinder(radius = 3.74615f, height = 2.51563f) _ val bailableSeat = new SeatDefinition() { bailable = true @@ -5694,6 +5715,11 @@ object GlobalDefinitions { fury.MaxDepth = 1.3f fury.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) fury.Geometry = atvForm + fury.collision.avatarCollisionDamageMax = 35 + fury.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 5), (0.5f, 20), (0.75f, 40), (1f, 60))) + fury.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) + fury.maxForwardSpeed = 90f + fury.mass = 32.1f quadassault.Name = "quadassault" // Basilisk quadassault.MaxHealth = 650 @@ -5725,6 +5751,11 @@ object GlobalDefinitions { quadassault.MaxDepth = 1.3f quadassault.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) quadassault.Geometry = atvForm + quadassault.collision.avatarCollisionDamageMax = 35 + quadassault.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 5), (0.5f, 20), (0.75f, 40), (1f, 60))) + quadassault.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) + quadassault.maxForwardSpeed = 90f + quadassault.mass = 32.1f quadstealth.Name = "quadstealth" // Wraith quadstealth.MaxHealth = 650 @@ -5756,6 +5787,11 @@ object GlobalDefinitions { quadstealth.MaxDepth = 1.25f quadstealth.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) quadstealth.Geometry = atvForm + quadstealth.collision.avatarCollisionDamageMax = 35 + quadstealth.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 5), (0.5f, 20), (0.75f, 40), (1f, 60))) + quadstealth.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) + quadstealth.maxForwardSpeed = 90f + quadstealth.mass = 32.1f two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser two_man_assault_buggy.MaxHealth = 1250 @@ -5788,6 +5824,11 @@ object GlobalDefinitions { two_man_assault_buggy.MaxDepth = 1.5f two_man_assault_buggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) two_man_assault_buggy.Geometry = GeometryForm.representByCylinder(radius = 2.10545f, height = 1.59376f) + two_man_assault_buggy.collision.avatarCollisionDamageMax = 75 + two_man_assault_buggy.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 5), (0.5f, 20), (0.75f, 40), (1f, 60))) + two_man_assault_buggy.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.5f, 600))) + two_man_assault_buggy.maxForwardSpeed = 85f + two_man_assault_buggy.mass = 52.4f skyguard.Name = "skyguard" skyguard.MaxHealth = 1000 @@ -5821,6 +5862,11 @@ object GlobalDefinitions { skyguard.MaxDepth = 1.5f skyguard.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) skyguard.Geometry = GeometryForm.representByCylinder(radius = 1.8867f, height = 1.4375f) + skyguard.collision.avatarCollisionDamageMax = 100 + skyguard.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 5), (0.5f, 20), (0.75f, 40), (1f, 60))) + skyguard.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.4f, 600))) + skyguard.maxForwardSpeed = 90f + skyguard.mass = 78.9f threemanheavybuggy.Name = "threemanheavybuggy" // Marauder threemanheavybuggy.MaxHealth = 1700 @@ -5858,6 +5904,11 @@ object GlobalDefinitions { threemanheavybuggy.MaxDepth = 1.83f threemanheavybuggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) threemanheavybuggy.Geometry = GeometryForm.representByCylinder(radius = 2.1953f, height = 2.03125f) + threemanheavybuggy.collision.avatarCollisionDamageMax = 100 + threemanheavybuggy.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 15), (0.5f, 30), (0.75f, 60), (1f, 80))) + threemanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) + threemanheavybuggy.maxForwardSpeed = 80f + threemanheavybuggy.mass = 96.3f twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer twomanheavybuggy.MaxHealth = 1800 @@ -5891,6 +5942,11 @@ object GlobalDefinitions { twomanheavybuggy.MaxDepth = 1.95f twomanheavybuggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) twomanheavybuggy.Geometry = GeometryForm.representByCylinder(radius = 2.60935f, height = 1.79688f) + twomanheavybuggy.collision.avatarCollisionDamageMax = 100 + twomanheavybuggy.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 12), (0.5f, 30), (0.75f, 55), (1f, 80))) + twomanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) + twomanheavybuggy.maxForwardSpeed = 80f + twomanheavybuggy.mass = 83.2f twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher twomanhoverbuggy.MaxHealth = 1600 @@ -5926,6 +5982,11 @@ object GlobalDefinitions { recovery = 5000L ) //but the thresher hovers over water, so ...? twomanhoverbuggy.Geometry = GeometryForm.representByCylinder(radius = 2.1875f, height = 2.01563f) + twomanhoverbuggy.collision.avatarCollisionDamageMax = 125 + twomanhoverbuggy.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 13), (0.5f, 35), (0.75f, 65), (1f, 90))) + twomanhoverbuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) + twomanhoverbuggy.maxForwardSpeed = 85f + twomanhoverbuggy.mass = 55.5f mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.MaxHealth = 2500 @@ -5969,6 +6030,11 @@ object GlobalDefinitions { mediumtransport.MaxDepth = 1.2f mediumtransport.UnderwaterLifespan(suffocation = -1, recovery = -1) mediumtransport.Geometry = delivererForm + mediumtransport.collision.avatarCollisionDamageMax = 120 + mediumtransport.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 35), (0.5f, 60), (0.75f, 110), (1f, 175))) + mediumtransport.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) + mediumtransport.maxForwardSpeed = 70f + mediumtransport.mass = 108.5f battlewagon.Name = "battlewagon" // Raider battlewagon.MaxHealth = 2500 @@ -6015,6 +6081,11 @@ object GlobalDefinitions { battlewagon.MaxDepth = 1.2f battlewagon.UnderwaterLifespan(suffocation = -1, recovery = -1) battlewagon.Geometry = delivererForm + battlewagon.collision.avatarCollisionDamageMax = 120 + battlewagon.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 35), (0.5f, 60), (0.75f, 110), (1f, 175))) //inherited from mediumtransport + battlewagon.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) //inherited from mediumtransport + battlewagon.maxForwardSpeed = 65f + battlewagon.mass = 108.5f thunderer.Name = "thunderer" thunderer.MaxHealth = 2500 @@ -6058,6 +6129,11 @@ object GlobalDefinitions { thunderer.MaxDepth = 1.2f thunderer.UnderwaterLifespan(suffocation = -1, recovery = -1) thunderer.Geometry = delivererForm + thunderer.collision.avatarCollisionDamageMax = 120 + thunderer.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 35), (0.5f, 60), (0.75f, 110), (1f, 175))) + thunderer.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) + thunderer.maxForwardSpeed = 65f + thunderer.mass = 108.5f aurora.Name = "aurora" aurora.MaxHealth = 2500 @@ -6101,6 +6177,11 @@ object GlobalDefinitions { aurora.MaxDepth = 1.2f aurora.UnderwaterLifespan(suffocation = -1, recovery = -1) aurora.Geometry = delivererForm + aurora.collision.avatarCollisionDamageMax = 120 + aurora.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 35), (0.5f, 60), (0.75f, 110), (1f, 175))) + aurora.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) + aurora.maxForwardSpeed = 65f + aurora.mass = 108.5f apc_tr.Name = "apc_tr" // Juggernaut apc_tr.MaxHealth = 6000 @@ -6164,6 +6245,11 @@ object GlobalDefinitions { apc_tr.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_tr.Geometry = apcForm apc_tr.MaxCapacitor = 300 + apc_tr.collision.avatarCollisionDamageMax = 300 + apc_tr.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 40), (0.75f, 70), (1f, 110))) + apc_tr.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) + apc_tr.maxForwardSpeed = 60f + apc_tr.mass = 128.4f apc_nc.Name = "apc_nc" // Vindicator apc_nc.MaxHealth = 6000 @@ -6227,6 +6313,11 @@ object GlobalDefinitions { apc_nc.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_nc.Geometry = apcForm apc_nc.MaxCapacitor = 300 + apc_nc.collision.avatarCollisionDamageMax = 300 + apc_nc.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 40), (0.75f, 70), (1f, 110))) + apc_nc.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) + apc_nc.maxForwardSpeed = 60f + apc_nc.mass = 128.4f apc_vs.Name = "apc_vs" // Leviathan apc_vs.MaxHealth = 6000 @@ -6290,6 +6381,11 @@ object GlobalDefinitions { apc_vs.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_vs.Geometry = apcForm apc_vs.MaxCapacitor = 300 + apc_vs.collision.avatarCollisionDamageMax = 300 + apc_vs.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 40), (0.75f, 70), (1f, 110))) + apc_vs.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) + apc_vs.maxForwardSpeed = 60f + apc_vs.mass = 128.4f lightning.Name = "lightning" lightning.MaxHealth = 2000 @@ -6324,6 +6420,11 @@ object GlobalDefinitions { lightning.MaxDepth = 1.38f lightning.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) lightning.Geometry = GeometryForm.representByCylinder(radius = 2.5078f, height = 1.79688f) + lightning.collision.avatarCollisionDamageMax = 150 + lightning.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 25), (0.75f, 50), (1f, 80))) + lightning.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 750))) + lightning.maxForwardSpeed = 74f + lightning.mass = 100.2f prowler.Name = "prowler" prowler.MaxHealth = 4800 @@ -6363,6 +6464,11 @@ object GlobalDefinitions { prowler.MaxDepth = 3 prowler.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) prowler.Geometry = GeometryForm.representByCylinder(radius = 3.461f, height = 3.48438f) + prowler.collision.avatarCollisionDamageMax = 300 + prowler.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 15), (0.5f, 40), (0.75f, 75), (1f, 100))) + prowler.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500))) + prowler.maxForwardSpeed = 57f + prowler.mass = 510.5f vanguard.Name = "vanguard" vanguard.MaxHealth = 5400 @@ -6398,6 +6504,11 @@ object GlobalDefinitions { vanguard.MaxDepth = 2.7f vanguard.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) vanguard.Geometry = GeometryForm.representByCylinder(radius = 3.8554f, height = 2.60938f) + vanguard.collision.avatarCollisionDamageMax = 300 + vanguard.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 5), (0.5f, 20), (0.75f, 40), (1f, 60))) + vanguard.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 100), (30f, 250), (32.5f, 600))) + vanguard.maxForwardSpeed = 60f + vanguard.mass = 460.4f magrider.Name = "magrider" magrider.MaxHealth = 4200 @@ -6435,6 +6546,11 @@ object GlobalDefinitions { magrider.MaxDepth = 2 magrider.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the magrider hovers over water, so ...? magrider.Geometry = GeometryForm.representByCylinder(radius = 3.3008f, height = 3.26562f) + magrider.collision.avatarCollisionDamageMax = 225 + magrider.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 35), (0.5f, 70), (0.75f, 90), (1f, 120))) + magrider.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500))) + magrider.maxForwardSpeed = 65f + magrider.mass = 75.3f val utilityConverter = new UtilityVehicleConverter ant.Name = "ant" @@ -6470,6 +6586,11 @@ object GlobalDefinitions { ant.MaxDepth = 2 ant.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) ant.Geometry = GeometryForm.representByCylinder(radius = 2.16795f, height = 2.09376f) //TODO hexahedron + ant.collision.avatarCollisionDamageMax = 50 + ant.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 30), (0.75f, 50), (1f, 70))) + ant.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 500), (13f, 750))) + ant.maxForwardSpeed = 65f + ant.mass = 80.5f ams.Name = "ams" ams.MaxHealth = 3000 @@ -6508,6 +6629,11 @@ object GlobalDefinitions { ams.MaxDepth = 3 ams.UnderwaterLifespan(suffocation = 5000L, recovery = 5000L) ams.Geometry = GeometryForm.representByCylinder(radius = 3.0117f, height = 3.39062f) //TODO hexahedron + ams.collision.avatarCollisionDamageMax = 250 + ams.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 40), (0.75f, 60), (1f, 100))) + ams.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 805), (13f, 3000))) + ams.maxForwardSpeed = 70f + ams.mass = 136.8f val variantConverter = new VariantVehicleConverter router.Name = "router" @@ -6543,8 +6669,13 @@ object GlobalDefinitions { } router.DrownAtMaxDepth = true router.MaxDepth = 2 - router.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the router hovers over water, so ...? + router.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the router hovers over water, so ...? router.Geometry = GeometryForm.representByCylinder(radius = 3.64845f, height = 3.51563f) //TODO hexahedron + router.collision.avatarCollisionDamageMax = 150 //it has to bonk you on the head when it falls? + router.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 13), (0.5f, 35), (0.75f, 65), (1f, 90))) + router.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900))) + router.maxForwardSpeed = 60f + router.mass = 60f switchblade.Name = "switchblade" switchblade.MaxHealth = 1750 @@ -6585,6 +6716,11 @@ object GlobalDefinitions { recovery = 5000L ) //but the switchblade hovers over water, so ...? switchblade.Geometry = GeometryForm.representByCylinder(radius = 2.4335f, height = 2.73438f) + switchblade.collision.avatarCollisionDamageMax = 35 + switchblade.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 13), (0.5f, 35), (0.75f, 65), (1f, 90))) + switchblade.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 800))) + switchblade.maxForwardSpeed = 80f + switchblade.mass = 63.9f flail.Name = "flail" flail.MaxHealth = 2400 @@ -6621,6 +6757,22 @@ object GlobalDefinitions { flail.MaxDepth = 2 flail.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the flail hovers over water, so ...? flail.Geometry = GeometryForm.representByCylinder(radius = 2.1875f, height = 2.21875f) + flail.collision.avatarCollisionDamageMax = 175 + flail.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 12), (0.5f, 35), (0.75f, 65), (1f, 90))) + flail.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900))) + flail.maxForwardSpeed = 55f + flail.mass = 73.5f + } + + /** + * Initialize flight `VehicleDefinition` globals. + */ + private def init_flight_vehicles(): Unit = { + val liberatorForm = GeometryForm.representByCylinder(radius = 3.74615f, height = 2.51563f) _ + val bailableSeat = new SeatDefinition() { + bailable = true + } + val variantConverter = new VariantVehicleConverter mosquito.Name = "mosquito" mosquito.MaxHealth = 665 @@ -6654,6 +6806,11 @@ object GlobalDefinitions { mosquito.DrownAtMaxDepth = true mosquito.MaxDepth = 2 //flying vehicles will automatically disable mosquito.Geometry = GeometryForm.representByCylinder(radius = 2.72108f, height = 2.5f) + mosquito.collision.avatarCollisionDamageMax = 50 + mosquito.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 50), (0.5f, 100), (0.75f, 150), (1f, 200))) + mosquito.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) + mosquito.maxForwardSpeed = 120f + mosquito.mass = 53.6f lightgunship.Name = "lightgunship" // Reaver lightgunship.MaxHealth = 855 // Temporary - Correct Reaver Health from pre-"Coder Madness 2" Event @@ -6688,6 +6845,11 @@ object GlobalDefinitions { lightgunship.DrownAtMaxDepth = true lightgunship.MaxDepth = 2 //flying vehicles will automatically disable lightgunship.Geometry = GeometryForm.representByCylinder(radius = 2.375f, height = 1.98438f) + lightgunship.collision.avatarCollisionDamageMax = 750 + lightgunship.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 60), (0.5f, 120), (0.75f, 180), (1f, 250))) + lightgunship.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) + lightgunship.maxForwardSpeed = 104f + lightgunship.mass = 51.1f wasp.Name = "wasp" wasp.MaxHealth = 515 @@ -6721,6 +6883,11 @@ object GlobalDefinitions { wasp.DrownAtMaxDepth = true wasp.MaxDepth = 2 //flying vehicles will automatically disable wasp.Geometry = GeometryForm.representByCylinder(radius = 2.88675f, height = 2.5f) + wasp.collision.avatarCollisionDamageMax = 50 //mosquito numbers + wasp.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 50), (0.5f, 100), (0.75f, 150), (1f, 200))) //mosquito numbers + wasp.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) //mosquito numbers + wasp.maxForwardSpeed = 120f + wasp.mass = 53.6f liberator.Name = "liberator" liberator.MaxHealth = 2500 @@ -6729,7 +6896,7 @@ object GlobalDefinitions { liberator.RepairIfDestroyed = false liberator.MaxShields = 500 liberator.CanFly = true - liberator.Seats += 0 -> new SeatDefinition() + liberator.Seats += 0 -> bailableSeat //new SeatDefinition() liberator.Seats += 1 -> bailableSeat liberator.Seats += 2 -> bailableSeat liberator.controlledWeapons += 0 -> 3 @@ -6763,6 +6930,11 @@ object GlobalDefinitions { liberator.DrownAtMaxDepth = true liberator.MaxDepth = 2 //flying vehicles will automatically disable liberator.Geometry = liberatorForm + liberator.collision.avatarCollisionDamageMax = 100 + liberator.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 60), (0.5f, 120), (0.75f, 180), (1f, 250))) + liberator.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) + liberator.maxForwardSpeed = 90f + liberator.mass = 82f vulture.Name = "vulture" vulture.MaxHealth = 2500 @@ -6771,7 +6943,7 @@ object GlobalDefinitions { vulture.RepairIfDestroyed = false vulture.MaxShields = 500 vulture.CanFly = true - vulture.Seats += 0 -> new SeatDefinition() + vulture.Seats += 0 -> bailableSeat //new SeatDefinition() vulture.Seats += 1 -> bailableSeat vulture.Seats += 2 -> bailableSeat vulture.controlledWeapons += 0 -> 3 @@ -6806,6 +6978,11 @@ object GlobalDefinitions { vulture.DrownAtMaxDepth = true vulture.MaxDepth = 2 //flying vehicles will automatically disable vulture.Geometry = liberatorForm + vulture.collision.avatarCollisionDamageMax = 100 + vulture.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 60), (0.5f, 120), (0.75f, 180), (1f, 250))) + vulture.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) + vulture.maxForwardSpeed = 97f + vulture.mass = 82f dropship.Name = "dropship" // Galaxy dropship.MaxHealth = 5000 @@ -6815,7 +6992,7 @@ object GlobalDefinitions { dropship.RepairIfDestroyed = false dropship.MaxShields = 1000 dropship.CanFly = true - dropship.Seats += 0 -> new SeatDefinition() + dropship.Seats += 0 -> bailableSeat //new SeatDefinition() dropship.Seats += 1 -> bailableSeat dropship.Seats += 2 -> bailableSeat dropship.Seats += 3 -> bailableSeat @@ -6876,6 +7053,11 @@ object GlobalDefinitions { dropship.DrownAtMaxDepth = true dropship.MaxDepth = 2 dropship.Geometry = GeometryForm.representByCylinder(radius = 10.52202f, height = 6.23438f) + dropship.collision.avatarCollisionDamageMax = 300 + dropship.collision.xy = CollisionXYData(Array((0.1f, 5), (0.25f, 125), (0.5f, 250), (0.75f, 500), (1f, 1000))) + dropship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) + dropship.maxForwardSpeed = 80f + dropship.mass = 133f galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.MaxHealth = 6000 @@ -6885,7 +7067,7 @@ object GlobalDefinitions { galaxy_gunship.RepairIfDestroyed = false galaxy_gunship.MaxShields = 1200 galaxy_gunship.CanFly = true - galaxy_gunship.Seats += 0 -> new SeatDefinition() + galaxy_gunship.Seats += 0 -> bailableSeat //new SeatDefinition() galaxy_gunship.Seats += 1 -> bailableSeat galaxy_gunship.Seats += 2 -> bailableSeat galaxy_gunship.Seats += 3 -> bailableSeat @@ -6930,6 +7112,11 @@ object GlobalDefinitions { galaxy_gunship.DrownAtMaxDepth = true galaxy_gunship.MaxDepth = 2 galaxy_gunship.Geometry = GeometryForm.representByCylinder(radius = 9.2382f, height = 5.01562f) + galaxy_gunship.collision.avatarCollisionDamageMax = 300 + galaxy_gunship.collision.xy = CollisionXYData(Array((0.1f, 5), (0.25f, 125), (0.5f, 250), (0.75f, 500), (1f, 1000))) + galaxy_gunship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) + galaxy_gunship.maxForwardSpeed = 85f + galaxy_gunship.mass = 133f lodestar.Name = "lodestar" lodestar.MaxHealth = 5000 @@ -6939,7 +7126,7 @@ object GlobalDefinitions { lodestar.RepairIfDestroyed = false lodestar.MaxShields = 1000 lodestar.CanFly = true - lodestar.Seats += 0 -> new SeatDefinition() + lodestar.Seats += 0 -> bailableSeat lodestar.MountPoints += 1 -> MountInfo(0) lodestar.MountPoints += 2 -> MountInfo(1) lodestar.Cargo += 1 -> new CargoDefinition() @@ -6972,6 +7159,9 @@ object GlobalDefinitions { lodestar.DrownAtMaxDepth = true lodestar.MaxDepth = 2 lodestar.Geometry = GeometryForm.representByCylinder(radius = 7.8671f, height = 6.79688f) //TODO hexahedron + lodestar.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) + lodestar.maxForwardSpeed = 80f + lodestar.mass = 128.2f phantasm.Name = "phantasm" phantasm.MaxHealth = 2500 @@ -7011,6 +7201,11 @@ object GlobalDefinitions { phantasm.DrownAtMaxDepth = true phantasm.MaxDepth = 2 phantasm.Geometry = GeometryForm.representByCylinder(radius = 5.2618f, height = 3f) + phantasm.collision.avatarCollisionDamageMax = 100 + phantasm.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 60), (0.5f, 120), (0.75f, 180), (1f, 250))) + phantasm.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) + phantasm.maxForwardSpeed = 140f + phantasm.mass = 100f droppod.Name = "droppod" droppod.MaxHealth = 20000 @@ -7027,6 +7222,7 @@ object GlobalDefinitions { droppod.DestroyedModel = None //the adb calls out a droppod; the cyclic nature of this confounds me droppod.DamageUsing = DamageCalculations.AgainstAircraft droppod.DrownAtMaxDepth = false + droppod.mass = 2500f orbital_shuttle.Name = "orbital_shuttle" orbital_shuttle.MaxHealth = 20000 @@ -7059,6 +7255,7 @@ object GlobalDefinitions { orbital_shuttle.DestroyedModel = None orbital_shuttle.DamageUsing = DamageCalculations.AgainstNothing orbital_shuttle.DrownAtMaxDepth = false + orbital_shuttle.mass = 25000f } /** @@ -7185,6 +7382,9 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } spitfire_turret.Geometry = smallTurret + spitfire_turret.collision.xy = CollisionXYData(Array((0.01f, 10), (0.02f, 40), (0.03f, 60), (0.04f, 80), (0.05f, 100))) + spitfire_turret.collision.z = CollisionZData(Array((4f, 10), (4.25f, 40), (4.5f, 60), (4.75f, 80), (5f, 100))) + spitfire_turret.mass = 5f spitfire_cloaked.Name = "spitfire_cloaked" spitfire_cloaked.Descriptor = "CloakingSpitfires" @@ -7209,6 +7409,9 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } spitfire_cloaked.Geometry = smallTurret + spitfire_cloaked.collision.xy = CollisionXYData(Array((0.01f, 10), (0.02f, 40), (0.03f, 60), (0.04f, 80), (0.05f, 100))) + spitfire_cloaked.collision.z = CollisionZData(Array((4f, 10), (4.25f, 40), (4.5f, 60), (4.75f, 80), (5f, 100))) + spitfire_cloaked.mass = 5f spitfire_aa.Name = "spitfire_aa" spitfire_aa.Descriptor = "FlakSpitfires" @@ -7233,6 +7436,9 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } spitfire_aa.Geometry = smallTurret + spitfire_aa.collision.xy = CollisionXYData(Array((0.01f, 10), (0.02f, 40), (0.03f, 60), (0.04f, 80), (0.05f, 100))) + spitfire_aa.collision.z = CollisionZData(Array((4f, 10), (4.25f, 40), (4.5f, 60), (4.75f, 80), (5f, 100))) + spitfire_aa.mass = 5f motionalarmsensor.Name = "motionalarmsensor" motionalarmsensor.Descriptor = "MotionSensors" @@ -7273,6 +7479,10 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } tank_traps.Geometry = GeometryForm.representByCylinder(radius = 2.89680997f, height = 3.57812f) + tank_traps.collision.xy = CollisionXYData(Array((0.01f, 5), (0.02f, 10), (0.03f, 15), (0.04f, 20), (0.05f, 25))) + tank_traps.collision.z = CollisionZData(Array((4f, 10), (4.25f, 40), (4.5f, 60), (4.75f, 80), (5f, 100))) + tank_traps.Modifiers = TrapCollisionDamageMultiplier(5f) //10f + tank_traps.mass = 600f val fieldTurretConverter = new FieldTurretConverter portable_manned_turret.Name = "portable_manned_turret" @@ -7303,6 +7513,9 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } portable_manned_turret.Geometry = largeTurret + portable_manned_turret.collision.xy = CollisionXYData(Array((0.01f, 10), (0.02f, 40), (0.03f, 60), (0.04f, 80), (0.05f, 100))) + portable_manned_turret.collision.z = CollisionZData(Array((4f, 10), (4.25f, 40), (4.5f, 60), (4.75f, 80), (5f, 100))) + portable_manned_turret.mass = 100f portable_manned_turret_nc.Name = "portable_manned_turret_nc" portable_manned_turret_nc.Descriptor = "FieldTurrets" @@ -7332,6 +7545,9 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } portable_manned_turret_nc.Geometry = largeTurret + portable_manned_turret_nc.collision.xy = CollisionXYData(Array((0.01f, 10), (0.02f, 40), (0.03f, 60), (0.04f, 80), (0.05f, 100))) + portable_manned_turret_nc.collision.z = CollisionZData(Array((4f, 10), (4.25f, 40), (4.5f, 60), (4.75f, 80), (5f, 100))) + portable_manned_turret_nc.mass = 100f portable_manned_turret_tr.Name = "portable_manned_turret_tr" portable_manned_turret_tr.Descriptor = "FieldTurrets" @@ -7361,6 +7577,9 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } portable_manned_turret_tr.Geometry = largeTurret + portable_manned_turret_tr.collision.xy = CollisionXYData(Array((0.01f, 10), (0.02f, 40), (0.03f, 60), (0.04f, 80), (0.05f, 100))) + portable_manned_turret_tr.collision.z = CollisionZData(Array((4f, 10), (4.25f, 40), (4.5f, 60), (4.75f, 80), (5f, 100))) + portable_manned_turret_tr.mass = 100f portable_manned_turret_vs.Name = "portable_manned_turret_vs" portable_manned_turret_vs.Descriptor = "FieldTurrets" @@ -7390,6 +7609,9 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } portable_manned_turret_vs.Geometry = largeTurret + portable_manned_turret_vs.collision.xy = CollisionXYData(Array((0.01f, 10), (0.02f, 40), (0.03f, 60), (0.04f, 80), (0.05f, 100))) + portable_manned_turret_vs.collision.z = CollisionZData(Array((4f, 10), (4.25f, 40), (4.5f, 60), (4.75f, 80), (5f, 100))) + portable_manned_turret_vs.mass = 100f deployable_shield_generator.Name = "deployable_shield_generator" deployable_shield_generator.Descriptor = "ShieldGenerators" diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 71b7c66e3..05403a288 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -10,6 +10,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.aura.AuraContainer import net.psforever.objects.serverobject.environment.InteractWithEnvironment +import net.psforever.objects.serverobject.mount.MountableEntity import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.interaction.DamageInteraction @@ -31,9 +32,10 @@ class Player(var avatar: Avatar) with Container with JammableUnit with ZoneAware - with AuraContainer { + with AuraContainer + with MountableEntity { interaction(new InteractWithEnvironment()) - interaction(new InteractWithMines(range = 10)) + interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10)) private var backpack: Boolean = false private var released: Boolean = false @@ -594,3 +596,14 @@ object Player { } } } + +private class InteractWithMinesUnlessSpectating( + private val obj: Player, + range: Float + ) extends InteractWithMines(range) { + override def interaction(target: InteractsWithZone): Unit = { + if (!obj.spectator) { + super.interaction(target) + } + } +} diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index c363e625c..013092633 100644 --- a/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -101,6 +101,8 @@ class TurretControl(turret: TurretDeployable) override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = { super.DestructionAwareness(target, cause) + CancelJammeredSound(target) + CancelJammeredStatus(target) Deployables.AnnounceDestroyDeployable(turret, None) } diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index f21ecebea..b2fdd83a7 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -5,7 +5,7 @@ import net.psforever.objects.ce.InteractWithMines import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition} import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} -import net.psforever.objects.serverobject.mount.{Seat, SeatDefinition} +import net.psforever.objects.serverobject.mount.{MountableEntity, Seat, SeatDefinition} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.aura.AuraContainer @@ -86,7 +86,8 @@ class Vehicle(private val vehicleDef: VehicleDefinition) with JammableUnit with CommonNtuContainer with Container - with AuraContainer { + with AuraContainer + with MountableEntity { interaction(new InteractWithEnvironment()) interaction(new InteractWithMines(range = 30)) @@ -110,11 +111,11 @@ class Vehicle(private val vehicleDef: VehicleDefinition) private var utilities: Map[Int, Utility] = Map() private val trunk: GridInventory = GridInventory() - /** + /* * Records the GUID of the cargo vehicle (galaxy/lodestar) this vehicle is stored in for DismountVehicleCargoMsg use * DismountVehicleCargoMsg only passes the player_guid and this vehicle's guid */ - private var mountedIn: Option[PlanetSideGUID] = None + //private var mountedIn: Option[PlanetSideGUID] = None private var vehicleGatingManifest: Option[VehicleManifest] = None private var previousVehicleGatingManifest: Option[VehicleManifest] = None @@ -142,22 +143,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition) /** How long it takes to jack the vehicle in seconds, based on the hacker's certification level */ def JackingDuration: Array[Int] = Definition.JackingDuration - def MountedIn: Option[PlanetSideGUID] = { - this.mountedIn - } - - def MountedIn_=(cargo_vehicle_guid: PlanetSideGUID): Option[PlanetSideGUID] = MountedIn_=(Some(cargo_vehicle_guid)) - - def MountedIn_=(cargo_vehicle_guid: Option[PlanetSideGUID]): Option[PlanetSideGUID] = { - cargo_vehicle_guid match { - case Some(_) => - this.mountedIn = cargo_vehicle_guid - case None => - this.mountedIn = None - } - MountedIn - } - override def Health_=(assignHealth: Int): Int = { //TODO should vehicle class enforce this? if (!Destroyed) { @@ -499,6 +484,10 @@ class Vehicle(private val vehicleDef: VehicleDefinition) def DamageModel = Definition.asInstanceOf[DamageResistanceModel] + override def BailProtection_=(protect: Boolean): Boolean = { + !Definition.CanFly && super.BailProtection_=(protect) + } + /** * This is the definition entry that is used to store and unload pertinent information about the `Vehicle`. * @return the vehicle's definition entry diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index eb9c659a9..dc4ee5cdb 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -232,25 +232,13 @@ object Vehicles { log.info(s"${hacker.Name} has jacked a ${target.Definition.Name}") val zone = target.Zone // Forcefully dismount any cargo - target.CargoHolds.values.foreach(cargoHold => { + target.CargoHolds.foreach { case (index, cargoHold) => cargoHold.occupant match { case Some(cargo: Vehicle) => - cargo.Seats(0).occupant match { - case Some(_: Player) => - CargoBehavior.HandleVehicleCargoDismount( - target.Zone, - cargo.GUID, - bailed = target.isFlying, - requestedByPassenger = false, - kicked = true - ) - case _ => - log.error("FinishHackingVehicle: vehicle in cargo hold missing driver") - CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, target.GUID, target, bailed = false, requestedByPassenger = false, kicked = true) - } + cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false) case None => ; } - }) + } // Forcefully dismount all seated occupants from the vehicle target.Seats.values.foreach(seat => { seat.occupant match { diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 4f653bf72..bf787bdce 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -32,6 +32,7 @@ import net.psforever.objects.locker.LockerContainerControl import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.repair.Repairable import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad +import net.psforever.objects.vital.collision.CollisionReason import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} @@ -416,7 +417,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm (afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction } afterHolsters.foreach { case InventoryItem(citem: ConstructionItem, _) => - Deployables.initializeConstructionAmmoMode(player.avatar.certifications, citem) + Deployables.initializeConstructionItem(player.avatar.certifications, citem) case _ => ; } toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL } @@ -795,7 +796,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm case _ => cause.interaction.cause.source.Aggravated.nonEmpty } - //log historical event + //log historical event (always) target.History(cause) //stat changes if (damageToCapacitor > 0) { @@ -803,11 +804,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm target.Name, AvatarAction.PlanetsideAttributeSelf(targetGUID, 7, target.Capacitor.toLong) ) - announceConfrontation = true + announceConfrontation = true //TODO should we? } if (damageToStamina > 0) { avatarActor ! AvatarActor.ConsumeStamina(damageToStamina) - announceConfrontation = true + announceConfrontation = true //TODO should we? } if (damageToHealth > 0) { events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 0, health)) @@ -815,14 +816,19 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } val countableDamage = damageToHealth + damageToArmor if(announceConfrontation) { - if (!aggravated) { + if (aggravated) { + events ! AvatarServiceMessage( + zoneId, + AvatarAction.SendResponse(Service.defaultPlayerGUID, AggravatedDamageMessage(targetGUID, countableDamage)) + ) + } else { //activity on map zone.Activity ! Zone.HotSpot.Activity(cause) //alert to damage source cause.adversarial match { case Some(adversarial) => adversarial.attacker match { - case pSource : PlayerSource => //player damage + case pSource: PlayerSource => //player damage val name = pSource.Name zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match { case Some(tplayer) => @@ -855,6 +861,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm target.Name, AvatarAction.EnvironmentalDamage(target.GUID, o.entity.GUID, countableDamage) ) + case _: CollisionReason => + events ! AvatarServiceMessage( + zoneId, + AvatarAction.SendResponse(Service.defaultPlayerGUID, AggravatedDamageMessage(targetGUID, countableDamage)) + ) case _ => zone.AvatarEvents ! AvatarServiceMessage( target.Name, @@ -863,19 +874,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } } } - else { - //general alert - zone.AvatarEvents ! AvatarServiceMessage( - target.Name, - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, Vector3.Zero)) - ) - } - } - if (aggravated) { - events ! AvatarServiceMessage( - zoneId, - AvatarAction.SendResponse(Service.defaultPlayerGUID, AggravatedDamageMessage(targetGUID, countableDamage)) - ) } } @@ -1137,7 +1135,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //can not preserve ammo type in construction tool packets citem.resetAmmoTypes() } - Deployables.initializeConstructionAmmoMode(player.avatar.certifications, citem) + Deployables.initializeConstructionItem(player.avatar.certifications, citem) case _ => ; } diff --git a/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala b/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala index 26a6fcdc5..6eaa5a619 100644 --- a/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala +++ b/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala @@ -11,7 +11,7 @@ final case class DeployableSource( faction: PlanetSideEmpire.Value, health: Int, shields: Int, - ownerName: String, + owner: SourceEntry, position: Vector3, orientation: Vector3 ) extends SourceEntry { @@ -20,7 +20,7 @@ final case class DeployableSource( def Definition: ObjectDefinition with DeployableDefinition = obj_def def Health = health def Shields = shields - def OwnerName = ownerName + def OwnerName = owner.Name def Position = position def Orientation = orientation def Velocity = None @@ -29,12 +29,19 @@ final case class DeployableSource( object DeployableSource { def apply(obj: Deployable): DeployableSource = { + val ownerName = obj.OwnerName + val ownerSource = (obj.Zone.LivePlayers ++ obj.Zone.Corpses) + .find { p => ownerName.contains(p.Name) } + match { + case Some(p) => SourceEntry(p) + case _ => SourceEntry.None + } DeployableSource( obj.Definition, obj.Faction, obj.Health, obj.Shields, - obj.OwnerName.getOrElse(""), + ownerSource, obj.Position, obj.Orientation ) diff --git a/src/main/scala/net/psforever/objects/ballistics/VehicleSource.scala b/src/main/scala/net/psforever/objects/ballistics/VehicleSource.scala index b6f4ad6f8..d347bfb1b 100644 --- a/src/main/scala/net/psforever/objects/ballistics/VehicleSource.scala +++ b/src/main/scala/net/psforever/objects/ballistics/VehicleSource.scala @@ -14,6 +14,7 @@ final case class VehicleSource( position: Vector3, orientation: Vector3, velocity: Option[Vector3], + occupants: List[SourceEntry], modifiers: ResistanceProfile ) extends SourceEntry { override def Name = SourceEntry.NameFormat(obj_def.Name) @@ -37,6 +38,12 @@ object VehicleSource { obj.Position, obj.Orientation, obj.Velocity, + obj.Seats.values.map { seat => + seat.occupant match { + case Some(p) => PlayerSource(p) + case _ => SourceEntry.None + } + }.toList, obj.Definition.asInstanceOf[ResistanceProfile] ) } diff --git a/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala b/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala index 8600bdff0..4f9308292 100644 --- a/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/DeployableDefinition.scala @@ -8,7 +8,7 @@ import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.resistance.ResistanceProfileMutators import net.psforever.objects.vital.resolution.DamageResistanceModel -import net.psforever.objects.vital.{NoResistanceSelection, VitalityDefinition} +import net.psforever.objects.vital.{CollisionXYData, NoResistanceSelection, VitalityDefinition} import scala.concurrent.duration._ @@ -60,6 +60,7 @@ abstract class DeployableDefinition(objectId: Int) ResistUsing = NoResistanceSelection Packet = new SmallDeployableConverter registerAs = "deployables" + collision.xy = new CollisionXYData(List((0f, 100))) def Item: DeployedItem.Value = item } diff --git a/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala b/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala index 1fd9056d8..7a26c32bc 100644 --- a/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala @@ -29,6 +29,7 @@ class ExoSuitDefinition(private val suitType: ExoSuitType.Value) protected var capacitorRechargeDelayMillis: Int = 0 protected var capacitorRechargePerSecond: Int = 0 protected var capacitorDrainPerSecond: Int = 0 + val collision: ExosuitCollisionData = new ExosuitCollisionData() Name = "exo-suit" DamageUsing = DamageCalculations.AgainstExoSuit ResistUsing = StandardInfantryResistance diff --git a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala index c42e53ab1..23578022f 100644 --- a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala @@ -101,5 +101,15 @@ abstract class ObjectDefinition(private val objectId: Int) extends BasicDefiniti Geometry } + /** + * The maximum forward speed that can be expected to be achieved by this unit. + * Faster speeds are not discounted due to conditions of the motion or game environment + * but speeds too far beyond this measure should be considered suspicious. + * For ground vehicles, this field is called `maxForward` in the ADB. + * For flight vehicles, this field is called `MaxSpeed` and `flightmaxspeed` in the ADB, + * and it does not factor in the afterburner. + */ + var maxForwardSpeed: Float = 0f + def ObjectId: Int = objectId } diff --git a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index feddf851f..f5cbf394e 100644 --- a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -197,6 +197,8 @@ class VehicleDefinition(objectId: Int) obj.Actor ! akka.actor.PoisonPill obj.Actor = Default.Actor } + + override val collision: AdvancedCollisionData = new AdvancedCollisionData() } object VehicleDefinition { diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index 9efe3dc82..b38056602 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -5,6 +5,7 @@ import akka.actor.Actor import net.psforever.objects.{Vehicle, Vehicles} import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.vital.base.DamageResolution import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.services.Service @@ -103,13 +104,13 @@ trait DamageableVehicle case _ => (0, 0, 0) } var announceConfrontation: Boolean = reportDamageToVehicle || totalDamage > 0 - val aggravated = TryAggravationEffectActivate(cause) match { + val showAsAggravated = (TryAggravationEffectActivate(cause) match { case Some(_) => announceConfrontation = true false case _ => cause.interaction.cause.source.Aggravated.nonEmpty - } + }) || cause.interaction.cause.resolution == DamageResolution.Collision reportDamageToVehicle = false if (obj.MountedIn.nonEmpty) { @@ -139,7 +140,7 @@ trait DamageableVehicle } } if (announceConfrontation) { - if (aggravated) { + if (showAsAggravated) { val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(totalDamage, Vector3.Zero)) obj.Seats.values .collect { case seat if seat.occupant.nonEmpty => seat.occupant.get.Name } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala index 16211210e..3d858c4d6 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala @@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.mount import akka.actor.ActorRef import net.psforever.objects.Player +import net.psforever.types.BailType import scala.annotation.tailrec @@ -97,7 +98,11 @@ object Mountable { * @param player the player who sent this request message * @param seat_num the seat index */ - final case class TryDismount(player: Player, seat_num: Int) + final case class TryDismount(player: Player, seat_num: Int, bailType: BailType.Value) + + object TryDismount { + def apply(player: Player, seatNum: Int): TryDismount = TryDismount(player, seatNum, BailType.Normal) + } /** * A basic `Trait` connecting all of the actionable `Mountable` response messages. diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala index 7617efa73..0fb697d23 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala @@ -7,6 +7,7 @@ import net.psforever.objects.Player import net.psforever.objects.entity.WorldEntity import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.hackable.Hackable +import net.psforever.types.BailType import scala.collection.mutable @@ -82,9 +83,9 @@ trait MountableBehavior { * @see `Mountable` */ val dismountBehavior: Receive = { - case Mountable.TryDismount(user, seat_number) => + case Mountable.TryDismount(user, seat_number, bail_type) => val obj = MountableObject - if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user)) { + if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user, bail_type)) { user.VehicleSeated = None obj.Zone.actor ! ZoneActor.AddToBlockMap(user, obj.Position) sender() ! Mountable.MountMessages( @@ -112,11 +113,12 @@ trait MountableBehavior { private def tryDismount( obj: Mountable, seatNumber: Int, - user: Player + user: Player, + bailType: BailType.Value ): Boolean = { obj.Seats.get(seatNumber) match { - case Some(seat) => seat.unmount(user).isEmpty - case _ => false + case Some(seat) => seat.unmount(user, bailType).isEmpty + case _ => false } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableEntity.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableEntity.scala new file mode 100644 index 000000000..ed411a695 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableEntity.scala @@ -0,0 +1,26 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.types.PlanetSideGUID + +trait MountableEntity { + private var bailProtection: Boolean = false + + def BailProtection: Boolean = bailProtection + + def BailProtection_=(protect: Boolean) = { + bailProtection = protect + BailProtection + } + + private var mountedIn: Option[PlanetSideGUID] = None + + def MountedIn: Option[PlanetSideGUID] = mountedIn + + def MountedIn_=(cargo_guid: PlanetSideGUID): Option[PlanetSideGUID] = MountedIn_=(Some(cargo_guid)) + + def MountedIn_=(cargo_guid: Option[PlanetSideGUID]): Option[PlanetSideGUID] = { + mountedIn = cargo_guid + MountedIn + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala index 25c046ba7..0db8cd788 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala @@ -1,7 +1,9 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.serverobject.mount -trait MountableSpace[A] { +import net.psforever.types.BailType + +trait MountableSpace[A <: MountableEntity] { private var _occupant: Option[A] = None /** @@ -53,6 +55,7 @@ trait MountableSpace[A] { target match { case Some(p) if testToMount(p) => _occupant = target + p.BailProtection = false target case _ => occupant @@ -63,7 +66,7 @@ trait MountableSpace[A] { * Tests whether the target is allowed to be mounted. * @see `MountableSpace[A].canBeOccupiedBy(A)` */ - protected def testToMount(target: A): Boolean = canBeOccupied && canBeOccupiedBy(target) + protected def testToMount(target: A): Boolean = target.MountedIn.isEmpty && canBeOccupied && canBeOccupiedBy(target) /** * Attempt to dismount the target entity from this space. @@ -73,10 +76,22 @@ trait MountableSpace[A] { /** * Attempt to dismount the target entity from this space. */ - def unmount(target: Option[A]): Option[A] = { + def unmount(target: A, bailType: BailType.Value): Option[A] = unmount(Some(target), bailType) + + /** + * Attempt to dismount the target entity from this space. + */ + def unmount(target: Option[A]): Option[A] = unmount(target, BailType.Normal) + + /** + * Attempt to dismount the target entity from this space. + * @return the current seat occupant, which should be `None` if the operation was successful + */ + def unmount(target: Option[A], bailType: BailType.Value): Option[A] = { target match { case Some(p) if testToUnmount(p) => _occupant = None + p.BailProtection = bailable && (bailType == BailType.Bailed || bailType == BailType.Kicked) None case _ => occupant diff --git a/src/main/scala/net/psforever/objects/vehicles/Cargo.scala b/src/main/scala/net/psforever/objects/vehicles/Cargo.scala index 736b771e4..e11dce036 100644 --- a/src/main/scala/net/psforever/objects/vehicles/Cargo.scala +++ b/src/main/scala/net/psforever/objects/vehicles/Cargo.scala @@ -5,7 +5,5 @@ import net.psforever.objects.Vehicle import net.psforever.objects.serverobject.mount.{MountableSpace, MountableSpaceDefinition} class Cargo(private val cdef: MountableSpaceDefinition[Vehicle]) extends MountableSpace[Vehicle] { - override protected def testToMount(target: Vehicle): Boolean = target.MountedIn.isEmpty && super.testToMount(target) - def definition: MountableSpaceDefinition[Vehicle] = cdef } diff --git a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala index 900f5d691..47ea7cdca 100644 --- a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala @@ -1,24 +1,12 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.vehicles -import akka.actor.{Actor, Cancellable} -import net.psforever.actors.zone.ZoneActor -import net.psforever.objects.zones.Zone +import akka.actor.Actor import net.psforever.objects._ -import net.psforever.objects.vehicles.CargoBehavior.{CheckCargoDismount, CheckCargoMounting} -import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage} -import net.psforever.types.{CargoStatus, PlanetSideGUID, Vector3} -import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.services.Service -import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} - -import scala.concurrent.duration._ +import net.psforever.types.PlanetSideGUID trait CargoBehavior { _: Actor => - private var cargoMountTimer: Cancellable = Default.Cancellable - private var cargoDismountTimer: Cancellable = Default.Cancellable - /* gate-keep mounting behavior so that unit does not try to dismount as cargo, or mount different vehicle */ private var isMounting: Option[PlanetSideGUID] = None /* gate-keep dismounting behavior so that unit does not try to mount as cargo, or dismount from different vehicle */ @@ -26,547 +14,79 @@ trait CargoBehavior { def CargoObject: Vehicle + def endAllCargoOperations(): Unit = { + val obj = CargoObject + val zone = obj.Zone + zone.GUID(isMounting) match { + case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID) + case _ => ; + } + isMounting = None + zone.GUID(isDismounting) match { + case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID) + case _ => ; + } + isDismounting = None + startCargoDismounting(bailed = false) + } + val cargoBehavior: Receive = { - case CheckCargoMounting(carrier_guid, mountPoint, iteration) => - val obj = CargoObject - if ( - (isMounting.isEmpty || isMounting.contains(carrier_guid)) && isDismounting.isEmpty && - CargoBehavior.HandleCheckCargoMounting(obj.Zone, carrier_guid, obj.GUID, obj, mountPoint, iteration) - ) { - if (iteration == 0) { - //open the cargo bay door - obj.Zone.AvatarEvents ! AvatarServiceMessage( - obj.Zone.id, - AvatarAction.SendResponse( - Service.defaultPlayerGUID, - CargoMountPointStatusMessage( - carrier_guid, - PlanetSideGUID(0), - obj.GUID, - PlanetSideGUID(0), - mountPoint, - CargoStatus.InProgress, - 0 - ) - ) - ) - } - isMounting = Some(carrier_guid) - import scala.concurrent.ExecutionContext.Implicits.global - cargoMountTimer.cancel() - cargoMountTimer = context.system.scheduler.scheduleOnce( - 250 milliseconds, - self, - CheckCargoMounting(carrier_guid, mountPoint, iteration + 1) - ) - } else { + case CargoBehavior.StartCargoMounting(carrier_guid, mountPoint) => + startCargoMounting(carrier_guid, mountPoint) + + case CargoBehavior.StartCargoDismounting(bailed) => + startCargoDismounting(bailed) + + case CargoBehavior.EndCargoMounting(carrier_guid) => + if (isMounting.contains(carrier_guid)) { isMounting = None } - case CheckCargoDismount(carrier_guid, mountPoint, iteration) => - val obj = CargoObject - if ( - (isDismounting.isEmpty || isDismounting.contains(carrier_guid)) && isMounting.isEmpty && - CargoBehavior.HandleCheckCargoDismounting(obj.Zone, carrier_guid, obj.GUID, obj, mountPoint, iteration) - ) { - isDismounting = Some(carrier_guid) - import scala.concurrent.ExecutionContext.Implicits.global - cargoDismountTimer.cancel() - cargoDismountTimer = context.system.scheduler.scheduleOnce( - 250 milliseconds, - self, - CheckCargoDismount(carrier_guid, mountPoint, iteration + 1) - ) - } else { + case CargoBehavior.EndCargoDismounting(carrier_guid) => + if (isDismounting.contains(carrier_guid)) { isDismounting = None } } + + def startCargoMounting(carrier_guid: PlanetSideGUID, mountPoint: Int): Unit = { + val obj = CargoObject + obj.Zone.GUID(carrier_guid) match { + case Some(carrier: Vehicle) + if isMounting.isEmpty && isDismounting.isEmpty && (carrier.CargoHolds.get(mountPoint) match { + case Some(hold) => !hold.isOccupied + case _ => false + }) => + isMounting = Some(carrier_guid) + carrier.Actor ! CarrierBehavior.CheckCargoMounting(obj.GUID, mountPoint, 0) + case _ => ; + isMounting = None + } + } + + def startCargoDismounting(bailed: Boolean): Unit = { + val obj = CargoObject + obj.Zone.GUID(obj.MountedIn) match { + case Some(carrier: Vehicle) => + carrier.CargoHolds.find { case (_, hold) => hold.occupant.contains(obj) } match { + case Some((mountPoint, _)) + if isDismounting.isEmpty && isMounting.isEmpty => + isDismounting = obj.MountedIn + carrier.Actor ! CarrierBehavior.CheckCargoDismount(obj.GUID, mountPoint, 0, bailed) + + case _ => + obj.MountedIn = None + isDismounting = None + } + case _ => + obj.MountedIn = None + isDismounting = None + } + } } object CargoBehavior { - private val log = org.log4s.getLogger("CargoBehavior") - - final case class CheckCargoMounting(carrier_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int) - final case class CheckCargoDismount(carrier_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int) - - /** - * na - * @param carrierGUID the ferrying carrier vehicle - * @param cargoGUID the vehicle being ferried as cargo - * @param cargo the vehicle being ferried as cargo - * @param mountPoint the cargo hold to which the cargo vehicle is stowed - * @param iteration number of times a proper mounting for this combination has been queried - */ - def HandleCheckCargoMounting( - zone: Zone, - carrierGUID: PlanetSideGUID, - cargoGUID: PlanetSideGUID, - cargo: Vehicle, - mountPoint: Int, - iteration: Int - ): Boolean = { - zone.GUID(carrierGUID) match { - case Some(carrier: Vehicle) => - HandleCheckCargoMounting(cargoGUID, cargo, carrierGUID, carrier, mountPoint, iteration) - case carrier if iteration > 0 => - log.warn(s"HandleCheckCargoMounting: participant vehicles changed in the middle of a mounting event") - LogCargoEventMissingVehicleError("HandleCheckCargoMounting: carrier", carrier, carrierGUID) - false - case _ => - false - } - } - - /** - * na - * @param cargoGUID the vehicle being ferried as cargo - * @param cargo the vehicle being ferried as cargo - * @param carrierGUID the ferrying carrier vehicle - * @param carrier the ferrying carrier vehicle - * @param mountPoint the cargo hold to which the cargo vehicle is stowed - * @param iteration number of times a proper mounting for this combination has been queried - */ - private def HandleCheckCargoMounting( - cargoGUID: PlanetSideGUID, - cargo: Vehicle, - carrierGUID: PlanetSideGUID, - carrier: Vehicle, - mountPoint: Int, - iteration: Int - ): Boolean = { - val zone = carrier.Zone - val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position) - carrier.CargoHold(mountPoint) match { - case Some(hold) if !hold.isOccupied => - log.debug( - s"HandleCheckCargoMounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=64" - ) - if (distance <= 64) { - //cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it - log.debug(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance") - hold.mount(cargo) - cargo.MountedIn = carrierGUID - cargo.Velocity = None - zone.VehicleEvents ! VehicleServiceMessage( - s"${cargo.Actor}", - VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health)) - ) - zone.VehicleEvents ! VehicleServiceMessage( - s"${cargo.Actor}", - VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) - ) - CargoMountBehaviorForAll(carrier, cargo, mountPoint) - zone.actor ! ZoneActor.RemoveFromBlockMap(cargo) - false - } else if (distance > 625 || iteration >= 40) { - //vehicles moved too far away or took too long to get into proper position; abort mounting - log.debug( - "HandleCheckCargoMounting: cargo vehicle is too far away or didn't mount within allocated time - aborting" - ) - val cargoDriverGUID = cargo.Seats(0).occupant.get.GUID - zone.VehicleEvents ! VehicleServiceMessage( - zone.id, - VehicleAction.SendResponse( - cargoDriverGUID, - CargoMountPointStatusMessage( - carrierGUID, - PlanetSideGUID(0), - PlanetSideGUID(0), - cargoGUID, - mountPoint, - CargoStatus.Empty, - 0 - ) - ) - ) - false - //sending packet to the cargo vehicle's client results in player being lock in own vehicle - //player gets stuck as "always trying to remount the cargo hold" - //obviously, don't do this - } else { - //cargo vehicle still not in position but there is more time to wait; reschedule check - true - } - case None => - ; - log.warn(s"HandleCheckCargoMounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint") - false - case _ => - if (iteration == 0) { - log.warn( - s"HandleCheckCargoMounting: carrier vehicle $carrier already possesses cargo in hold #$mountPoint; this operation was initiated incorrectly" - ) - } else { - log.error( - s"HandleCheckCargoMounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40" - ) - } - false - } - } - - /** - * na - * @param cargoGUID na - * @param carrierGUID na - * @param mountPoint na - * @param iteration na - */ - def HandleCheckCargoDismounting( - zone: Zone, - carrierGUID: PlanetSideGUID, - cargoGUID: PlanetSideGUID, - cargo: Vehicle, - mountPoint: Int, - iteration: Int - ): Boolean = { - zone.GUID(carrierGUID) match { - case Some(carrier: Vehicle) => - HandleCheckCargoDismounting(cargoGUID, cargo, carrierGUID, carrier, mountPoint, iteration) - case carrier if iteration > 0 => - log.error(s"HandleCheckCargoDismounting: participant vehicles changed in the middle of a mounting event") - LogCargoEventMissingVehicleError("HandleCheckCargoDismounting: carrier", carrier, carrierGUID) - false - case _ => - false - } - } - - /** - * na - * @param cargoGUID na - * @param cargo na - * @param carrierGUID na - * @param carrier na - * @param mountPoint na - * @param iteration na - */ - private def HandleCheckCargoDismounting( - cargoGUID: PlanetSideGUID, - cargo: Vehicle, - carrierGUID: PlanetSideGUID, - carrier: Vehicle, - mountPoint: Int, - iteration: Int - ): Boolean = { - val zone = carrier.Zone - carrier.CargoHold(mountPoint) match { - case Some(hold) if !hold.isOccupied => - val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position) - log.debug( - s"HandleCheckCargoDismounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=225" - ) - if (distance > 225) { - //cargo vehicle has moved far enough away; close the carrier's hold door - log.debug( - s"HandleCheckCargoDismounting: dismount of cargo vehicle from carrier complete at distance of $distance" - ) - val cargoDriverGUID = cargo.Seats(0).occupant.get.GUID - zone.VehicleEvents ! VehicleServiceMessage( - zone.id, - VehicleAction.SendResponse( - cargoDriverGUID, - CargoMountPointStatusMessage( - carrierGUID, - PlanetSideGUID(0), - PlanetSideGUID(0), - cargoGUID, - mountPoint, - CargoStatus.Empty, - 0 - ) - ) - ) - false - //sending packet to the cargo vehicle's client results in player being lock in own vehicle - //player gets stuck as "always trying to remount the cargo hold" - //obviously, don't do this - } else if (iteration > 40) { - //cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold - hold.mount(cargo) - cargo.MountedIn = carrierGUID - CargoMountBehaviorForAll(carrier, cargo, mountPoint) - zone.actor ! ZoneActor.RemoveFromBlockMap(cargo) - false - } else { - //cargo vehicle did not move far away enough yet and there is more time to wait; reschedule check - true - } - case None => - log.warn(s"HandleCheckCargoDismounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint") - false - case _ => - if (iteration == 0) { - log.warn( - s"HandleCheckCargoDismounting: carrier vehicle $carrier will not discharge the cargo of hold #$mountPoint; this operation was initiated incorrectly" - ) - } else { - log.error( - s"HandleCheckCargoDismounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40" - ) - } - false - } - } - - /** - * na - * @param zone na - * @param cargo_guid na - * @param bailed na - * @param requestedByPassenger na - * @param kicked na - */ - def HandleVehicleCargoDismount( - zone: Zone, - cargo_guid: PlanetSideGUID, - bailed: Boolean, - requestedByPassenger: Boolean, - kicked: Boolean - ): Unit = { - zone.GUID(cargo_guid) match { - case Some(cargo: Vehicle) => - zone.GUID(cargo.MountedIn) match { - case Some(ferry: Vehicle) => - HandleVehicleCargoDismount(cargo_guid, cargo, ferry.GUID, ferry, bailed, requestedByPassenger, kicked) - case _ => - log.warn( - s"DismountVehicleCargo: target ${cargo.Definition.Name}@$cargo_guid does not know what treats it as cargo" - ) - } - case _ => - log.warn(s"DismountVehicleCargo: target $cargo_guid either is not a vehicle in ${zone.id} or does not exist") - } - } - - /** - * na - * @param cargoGUID the globally unique number for the vehicle being ferried - * @param cargo the vehicle being ferried - * @param carrierGUID the globally unique number for the vehicle doing the ferrying - * @param carrier the vehicle doing the ferrying - * @param bailed the ferried vehicle is bailing from the cargo hold - * @param requestedByPassenger the ferried vehicle is being politely disembarked from the cargo hold - * @param kicked the ferried vehicle is being kicked out of the cargo hold - */ - def HandleVehicleCargoDismount( - cargoGUID: PlanetSideGUID, - cargo: Vehicle, - carrierGUID: PlanetSideGUID, - carrier: Vehicle, - bailed: Boolean, - requestedByPassenger: Boolean, - kicked: Boolean - ): Unit = { - val zone = carrier.Zone - carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match { - case Some((mountPoint, hold)) => - cargo.MountedIn = None - hold.unmount(cargo) - val driverOpt = cargo.Seats(0).occupant - val rotation: Vector3 = if (Vehicles.CargoOrientation(cargo) == 1) { //TODO: BFRs will likely also need this set - //dismount router "sideways" in a lodestar - carrier.Orientation.xy + Vector3.z((carrier.Orientation.z - 90) % 360) - } else { - carrier.Orientation - } - val cargoHoldPosition: Vector3 = if (carrier.Definition == GlobalDefinitions.dropship) { - //the galaxy cargo bay is offset backwards from the center of the vehicle - carrier.Position + Vector3.Rz(Vector3(0, 7, 0), math.toRadians(carrier.Orientation.z)) - } else { - //the lodestar's cargo hold is almost the center of the vehicle - carrier.Position - } - val GUID0 = Service.defaultPlayerGUID - val zoneId = zone.id - val events = zone.VehicleEvents - val cargoActor = cargo.Actor - events ! VehicleServiceMessage( - s"$cargoActor", - VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health)) - ) - events ! VehicleServiceMessage( - s"$cargoActor", - VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) - ) - zone.actor ! ZoneActor.AddToBlockMap(cargo, carrier.Position) - if (carrier.isFlying) { - //the carrier vehicle is flying; eject the cargo vehicle - val ejectCargoMsg = - CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.InProgress, 0) - val detachCargoMsg = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition - Vector3.z(1), rotation) - val resetCargoMsg = - CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.Empty, 0) - events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, ejectCargoMsg)) - events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, detachCargoMsg)) - events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, resetCargoMsg)) - log.debug(s"HandleVehicleCargoDismount: eject - $ejectCargoMsg, detach - $detachCargoMsg") - if (driverOpt.isEmpty) { - //TODO cargo should drop like a rock like normal; until then, deconstruct it - cargo.Actor ! Vehicle.Deconstruct() - } - } else { - //the carrier vehicle is not flying; just open the door and let the cargo vehicle back out; force it out if necessary - val cargoStatusMessage = - CargoMountPointStatusMessage(carrierGUID, GUID0, cargoGUID, GUID0, mountPoint, CargoStatus.InProgress, 0) - val cargoDetachMessage = - ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition + Vector3.z(1f), rotation) - events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, cargoStatusMessage)) - events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, cargoDetachMessage)) - driverOpt match { - case Some(driver) => - events ! VehicleServiceMessage( - s"${driver.Name}", - VehicleAction.KickCargo(GUID0, cargo, cargo.Definition.AutoPilotSpeed2, 2500) - ) - //check every quarter second if the vehicle has moved far enough away to be considered dismounted - cargoActor ! CheckCargoDismount(carrierGUID, mountPoint, 0) - case None => - val resetCargoMsg = - CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.Empty, 0) - events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, resetCargoMsg)) //lazy - //TODO cargo should back out like normal; until then, deconstruct it - cargoActor ! Vehicle.Deconstruct() - } - } - - case None => - log.warn(s"HandleDismountVehicleCargo: can not locate cargo $cargo in any hold of the carrier vehicle $carrier") - } - } - - //logging and messaging support functions - /** - * na - * @param decorator custom text for these messages in the log - * @param target an optional the target object - * @param targetGUID the expected globally unique identifier of the target object - */ - def LogCargoEventMissingVehicleError( - decorator: String, - target: Option[PlanetSideGameObject], - targetGUID: PlanetSideGUID - ): Unit = { - target match { - case Some(_: Vehicle) => ; - case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle") - case None => log.error(s"$decorator target $targetGUID has gone missing") - } - } - - /** - * Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - * that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @see `Vehicles.CargoOrientation` - * @param carrier the ferrying vehicle - * @param cargo the ferried vehicle - * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached; - * also known as a "cargo hold" - * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - */ - def CargoMountMessages( - carrier: Vehicle, - cargo: Vehicle, - mountPoint: Int - ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { - CargoMountMessages(carrier.GUID, cargo.GUID, mountPoint, Vehicles.CargoOrientation(cargo)) - } - - /** - * Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - * that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @param carrierGUID the ferrying vehicle - * @param cargoGUID the ferried vehicle - * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached - * @param orientation the positioning of the cargo vehicle in the carrier cargo bay - * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - */ - def CargoMountMessages( - carrierGUID: PlanetSideGUID, - cargoGUID: PlanetSideGUID, - mountPoint: Int, - orientation: Int - ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { - ( - ObjectAttachMessage(carrierGUID, cargoGUID, mountPoint), - CargoMountPointStatusMessage( - carrierGUID, - cargoGUID, - cargoGUID, - PlanetSideGUID(0), - mountPoint, - CargoStatus.Occupied, - orientation - ) - ) - } - - /** - * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @param carrier the ferrying vehicle - * @param cargo the ferried vehicle - * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached - * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - */ - def CargoMountBehaviorForOthers( - carrier: Vehicle, - cargo: Vehicle, - mountPoint: Int, - exclude: PlanetSideGUID - ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { - val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) - CargoMountMessagesForOthers(carrier.Zone, exclude, attachMessage, mountPointStatusMessage) - msgs - } - - /** - * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @param attachMessage an `ObjectAttachMessage` packet suitable for initializing cargo operations - * @param mountPointStatusMessage a `CargoMountPointStatusMessage` packet suitable for initializing cargo operations - */ - def CargoMountMessagesForOthers( - zone: Zone, - exclude: PlanetSideGUID, - attachMessage: ObjectAttachMessage, - mountPointStatusMessage: CargoMountPointStatusMessage - ): Unit = { - zone.VehicleEvents ! VehicleServiceMessage(zone.id, VehicleAction.SendResponse(exclude, attachMessage)) - zone.VehicleEvents ! VehicleServiceMessage(zone.id, VehicleAction.SendResponse(exclude, mountPointStatusMessage)) - } - - /** - * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to everyone. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @param carrier the ferrying vehicle - * @param cargo the ferried vehicle - * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached - * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - */ - def CargoMountBehaviorForAll( - carrier: Vehicle, - cargo: Vehicle, - mountPoint: Int - ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { - val zone = carrier.Zone - val zoneId = zone.id - val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) - zone.VehicleEvents ! VehicleServiceMessage( - zoneId, - VehicleAction.SendResponse(Service.defaultPlayerGUID, attachMessage) - ) - zone.VehicleEvents ! VehicleServiceMessage( - zoneId, - VehicleAction.SendResponse(Service.defaultPlayerGUID, mountPointStatusMessage) - ) - msgs - } + final case class StartCargoMounting(cargo_guid: PlanetSideGUID, cargo_mountpoint: Int) + final case class StartCargoDismounting(bailed: Boolean) + final case class EndCargoMounting(carrier_guid: PlanetSideGUID) + final case class EndCargoDismounting(carrier_guid: PlanetSideGUID) } diff --git a/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala b/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala deleted file mode 100644 index 483fc06ab..000000000 --- a/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.vehicles - -/** - * An `Enumeration` of exo-suit-based mount access restrictions.
- *
- * The default value is `NoMax` as that is the most common mount. - * `NoReinforcedOrMax` is next most common. - * `MaxOnly` is a rare mount restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. - */ -object CargoVehicleRestriction extends Enumeration { - type Type = Value - - val Small, Large = Value -} diff --git a/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala new file mode 100644 index 000000000..d2629f30b --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala @@ -0,0 +1,657 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles + +import akka.actor.{Actor, Cancellable} +import net.psforever.actors.zone.ZoneActor +import net.psforever.objects.zones.Zone +import net.psforever.objects._ +import net.psforever.packet.game.{ + CargoMountPointStatusMessage, + ObjectAttachMessage, + ObjectDetachMessage, + PlanetsideAttributeMessage +} +import net.psforever.types.{BailType, CargoStatus, PlanetSideGUID, Vector3} +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.Service +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} + +import scala.concurrent.duration._ + +trait CarrierBehavior { + _: Actor => + private var cargoMountTimer: Cancellable = Default.Cancellable + private var cargoDismountTimer: Cancellable = Default.Cancellable + + /* gate-keep mounting behavior so that another vehicle does not attempt to mount, or dismount in the middle */ + private var isMounting: Option[PlanetSideGUID] = None + /* gate-keep dismounting behavior so that another vehicle does not attempt to dismount, or dismount in the middle */ + private var isDismounting: Option[PlanetSideGUID] = None + + def CarrierObject: Vehicle + + def endAllCarrierOperations(): Unit = { + cargoMountTimer.cancel() + cargoDismountTimer.cancel() + val obj = CarrierObject + val zone = obj.Zone + zone.GUID(isMounting) match { + case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID) + case _ => ; + } + isMounting = None + zone.GUID(isDismounting) match { + case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID) + case _ => ; + } + isDismounting = None + } + + val carrierBehavior: Receive = { + case CarrierBehavior.CheckCargoMounting(cargo_guid, mountPoint, iteration) => + checkCargoMounting(cargo_guid, mountPoint, iteration) + + case CarrierBehavior.CheckCargoDismount(cargo_guid, mountPoint, iteration, bailed) => + checkCargoDismount(cargo_guid, mountPoint, iteration, bailed) + } + + def checkCargoMounting(cargo_guid: PlanetSideGUID, mountPoint: Int, iteration: Int): Unit = { + val obj = CarrierObject + if ( + (isMounting.isEmpty || isMounting.contains(cargo_guid)) && isDismounting.isEmpty && + CarrierBehavior.HandleCheckCargoMounting(obj.Zone, obj.GUID, cargo_guid, obj, mountPoint, iteration) + ) { + if (iteration == 0) { + //open the cargo bay door + obj.Zone.AvatarEvents ! AvatarServiceMessage( + obj.Zone.id, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + CargoMountPointStatusMessage( + obj.GUID, + PlanetSideGUID(0), + cargo_guid, + PlanetSideGUID(0), + mountPoint, + CargoStatus.InProgress, + 0 + ) + ) + ) + } + isMounting = Some(cargo_guid) + import scala.concurrent.ExecutionContext.Implicits.global + cargoMountTimer.cancel() + cargoMountTimer = context.system.scheduler.scheduleOnce( + 250 milliseconds, + self, + CarrierBehavior.CheckCargoMounting(cargo_guid, mountPoint, iteration + 1) + ) + } + else { + obj.Zone.GUID(isMounting) match { + case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID) + case _ => ; + } + isMounting = None + } + } + + def checkCargoDismount(cargo_guid: PlanetSideGUID, mountPoint: Int, iteration: Int, bailed: Boolean): Unit = { + val obj = CarrierObject + val zone = obj.Zone + val guid = obj.GUID + if ((isDismounting.isEmpty || isDismounting.contains(cargo_guid)) && isMounting.isEmpty) { + val prolongedDismount = if (iteration == 0) { + zone.GUID(cargo_guid) match { + case Some(cargo : Vehicle) => + CarrierBehavior.HandleVehicleCargoDismount( + cargo_guid, + cargo, + guid, + obj, + bailed, + requestedByPassenger = false, + kicked = false + ) + case _ => + obj.CargoHold(mountPoint) match { + case Some(hold) if hold.isOccupied && hold.occupant.get.GUID == cargo_guid => + hold.unmount(hold.occupant.get) + case _ => ; + } + false + } + } else { + CarrierBehavior.HandleCheckCargoDismounting(zone, guid, cargo_guid, obj, mountPoint, iteration, bailed) + } + if (prolongedDismount) { + isDismounting = Some(cargo_guid) + import scala.concurrent.ExecutionContext.Implicits.global + cargoDismountTimer.cancel() + cargoDismountTimer = context.system.scheduler.scheduleOnce( + 250 milliseconds, + self, + CarrierBehavior.CheckCargoDismount(cargo_guid, mountPoint, iteration + 1, bailed) + ) + } else { + zone.GUID(isDismounting.getOrElse(cargo_guid)) match { + case Some(cargo: Vehicle) => + cargo.Actor ! CargoBehavior.EndCargoDismounting(guid) + case _ => ; + } + isDismounting = None + } + } else { + zone.GUID(isDismounting.getOrElse(cargo_guid)) match { + case Some(cargo: Vehicle) => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid) + case _ => ; + } + isDismounting = None + } + } +} + +object CarrierBehavior { + private val log = org.log4s.getLogger(name = "CarrierBehavior") + + final case class CheckCargoMounting(cargo_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int) + final case class CheckCargoDismount(cargo_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int, bailed: Boolean) + + /** + * na + * @param carrierGUID the ferrying carrier vehicle + * @param cargoGUID the vehicle being ferried as cargo + * @param carrier the ferrying carrier vehicle + * @param mountPoint the cargo hold to which the cargo vehicle is stowed + * @param iteration number of times a proper mounting for this combination has been queried + */ + def HandleCheckCargoMounting( + zone: Zone, + carrierGUID: PlanetSideGUID, + cargoGUID: PlanetSideGUID, + carrier: Vehicle, + mountPoint: Int, + iteration: Int + ): Boolean = { + zone.GUID(cargoGUID) match { + case Some(cargo: Vehicle) => + HandleCheckCargoMounting(cargoGUID, cargo, carrierGUID, carrier, mountPoint, iteration) + case cargo if iteration > 0 => + log.warn(s"HandleCheckCargoMounting: participant vehicles changed in the middle of a mounting event") + LogCargoEventMissingVehicleError("HandleCheckCargoMounting: cargo", cargo, cargoGUID) + false + case _ => + false + } + } + + /** + * na + * @param cargoGUID the vehicle being ferried as cargo + * @param cargo the vehicle being ferried as cargo + * @param carrierGUID the ferrying carrier vehicle + * @param carrier the ferrying carrier vehicle + * @param mountPoint the cargo hold to which the cargo vehicle is stowed + * @param iteration number of times a proper mounting for this combination has been queried + */ + private def HandleCheckCargoMounting( + cargoGUID: PlanetSideGUID, + cargo: Vehicle, + carrierGUID: PlanetSideGUID, + carrier: Vehicle, + mountPoint: Int, + iteration: Int + ): Boolean = { + val zone = carrier.Zone + val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position) + carrier.CargoHold(mountPoint) match { + case Some(hold) if !hold.isOccupied && hold.canBeOccupiedBy(cargo) => + log.debug( + s"HandleCheckCargoMounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=64" + ) + if (distance <= 64) { + //cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it + log.debug(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance") + hold.mount(cargo) + cargo.MountedIn = carrierGUID + cargo.Velocity = None + cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID) + zone.VehicleEvents ! VehicleServiceMessage( + s"${cargo.Actor}", + VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health)) + ) + zone.VehicleEvents ! VehicleServiceMessage( + s"${cargo.Actor}", + VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) + ) + CargoMountBehaviorForAll(carrier, cargo, mountPoint) + zone.actor ! ZoneActor.RemoveFromBlockMap(cargo) + false + } else if (distance > 625 || iteration >= 40) { + //vehicles moved too far away or took too long to get into proper position; abort mounting + log.debug( + "HandleCheckCargoMounting: cargo vehicle is too far away or didn't mount within allocated time - aborting" + ) + cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID) + val cargoDriverGUID = cargo.Seats(0).occupant.get.GUID + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.SendResponse( + cargoDriverGUID, + CargoMountPointStatusMessage( + carrierGUID, + PlanetSideGUID(0), + PlanetSideGUID(0), + cargoGUID, + mountPoint, + CargoStatus.Empty, + 0 + ) + ) + ) + false + //sending packet to the cargo vehicle's client results in player being lock in own vehicle + //player gets stuck as "always trying to remount the cargo hold" + //obviously, don't do this + } else { + //cargo vehicle still not in position but there is more time to wait; reschedule check + true + } + case None => + log.warn(s"HandleCheckCargoMounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint") + cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID) + false + case _ => + if (iteration == 0) { + log.warn( + s"HandleCheckCargoMounting: carrier vehicle $carrier already possesses cargo in hold #$mountPoint; this operation was initiated incorrectly" + ) + } else { + log.error( + s"HandleCheckCargoMounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40" + ) + } + cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID) + false + } + } + + /** + * na + * @param cargoGUID na + * @param carrierGUID na + * @param mountPoint na + * @param iteration na + */ + def HandleCheckCargoDismounting( + zone: Zone, + carrierGUID: PlanetSideGUID, + cargoGUID: PlanetSideGUID, + carrier: Vehicle, + mountPoint: Int, + iteration: Int, + bailed: Boolean + ): Boolean = { + zone.GUID(cargoGUID) match { + case Some(cargo: Vehicle) => + HandleCheckCargoDismounting(cargoGUID, cargo, carrierGUID, carrier, mountPoint, iteration, bailed) + case cargo if iteration > 0 => + log.error(s"HandleCheckCargoDismounting: participant vehicles changed in the middle of a mounting event") + LogCargoEventMissingVehicleError("HandleCheckCargoDismounting: carrier", cargo, cargoGUID) + false + case _ => + false + } + } + + /** + * na + * @param cargoGUID na + * @param cargo na + * @param carrierGUID na + * @param carrier na + * @param mountPoint na + * @param iteration na + */ + private def HandleCheckCargoDismounting( + cargoGUID: PlanetSideGUID, + cargo: Vehicle, + carrierGUID: PlanetSideGUID, + carrier: Vehicle, + mountPoint: Int, + iteration: Int, + bailed: Boolean + ): Boolean = { + val zone = carrier.Zone + carrier.CargoHold(mountPoint) match { + case Some(hold) => + val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position) + log.debug( + s"HandleCheckCargoDismounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=225" + ) + if ((bailed && iteration > 0) || distance > 225) { + //cargo vehicle has moved far enough away; close the carrier's hold door + log.debug( + s"HandleCheckCargoDismounting: dismount of cargo vehicle from carrier complete at distance of $distance" + ) + cargo.Actor ! CargoBehavior.EndCargoDismounting(carrierGUID) + val cargoDriverGUID = cargo.Seats(0).occupant.get.GUID + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.SendResponse( + cargoDriverGUID, + CargoMountPointStatusMessage( + carrierGUID, + PlanetSideGUID(0), + PlanetSideGUID(0), + cargoGUID, + mountPoint, + CargoStatus.Empty, + 0 + ) + ) + ) + false + //sending packet to the cargo vehicle's client results in player being lock in own vehicle + //player gets stuck as "always trying to remount the cargo hold" + //obviously, don't do this + } else if (iteration > 40) { + //cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold + hold.mount(cargo) + cargo.MountedIn = carrierGUID + cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID) + CargoMountBehaviorForAll(carrier, cargo, mountPoint) + zone.actor ! ZoneActor.RemoveFromBlockMap(cargo) + false + } else { + //cargo vehicle did not move far away enough yet and there is more time to wait; reschedule check + true + } + case None => + log.warn(s"HandleCheckCargoDismounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint") + cargo.Actor ! CargoBehavior.EndCargoDismounting(carrierGUID) + false + case _ => + if (iteration == 0) { + log.warn( + s"HandleCheckCargoDismounting: carrier vehicle $carrier will not discharge the cargo of hold #$mountPoint; this operation was initiated incorrectly" + ) + } else { + log.error( + s"HandleCheckCargoDismounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40" + ) + } + cargo.Actor ! CargoBehavior.EndCargoDismounting(carrierGUID) + false + } + } + + /** + * na + * @param zone na + * @param cargo_guid na + * @param bailed na + * @param requestedByPassenger na + * @param kicked na + */ + def HandleVehicleCargoDismount( + zone: Zone, + cargo_guid: PlanetSideGUID, + bailed: Boolean, + requestedByPassenger: Boolean, + kicked: Boolean + ): Boolean = { + zone.GUID(cargo_guid) match { + case Some(cargo: Vehicle) => + zone.GUID(cargo.MountedIn) match { + case Some(ferry: Vehicle) => + HandleVehicleCargoDismount(cargo_guid, cargo, ferry.GUID, ferry, bailed, requestedByPassenger, kicked) + case _ => + log.warn( + s"DismountVehicleCargo: target ${cargo.Definition.Name}@$cargo_guid does not know what treats it as cargo" + ) + false + } + case _ => + log.warn(s"DismountVehicleCargo: target $cargo_guid either is not a vehicle in ${zone.id} or does not exist") + false + } + } + + /** + * na + * @param cargoGUID the globally unique number for the vehicle being ferried + * @param cargo the vehicle being ferried + * @param carrierGUID the globally unique number for the vehicle doing the ferrying + * @param carrier the vehicle doing the ferrying + * @param bailed the ferried vehicle is bailing from the cargo hold + * @param requestedByPassenger the ferried vehicle is being politely disembarked from the cargo hold + * @param kicked the ferried vehicle is being kicked out of the cargo hold + */ + def HandleVehicleCargoDismount( + cargoGUID: PlanetSideGUID, + cargo: Vehicle, + carrierGUID: PlanetSideGUID, + carrier: Vehicle, + bailed: Boolean, + requestedByPassenger: Boolean, + kicked: Boolean + ): Boolean = { + val zone = carrier.Zone + carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match { + case Some((mountPoint, hold)) => + cargo.MountedIn = None + hold.unmount( + cargo, + if (bailed) BailType.Bailed else if (kicked) BailType.Kicked else BailType.Normal + ) + val driverOpt = cargo.Seats(0).occupant + val rotation: Vector3 = if (Vehicles.CargoOrientation(cargo) == 1) { //TODO: BFRs will likely also need this set + //dismount router "sideways" from the lodestar + carrier.Orientation.xy + Vector3.z((carrier.Orientation.z - 90) % 360) + } else { + carrier.Orientation + } + val cargoHoldPosition: Vector3 = if (carrier.Definition == GlobalDefinitions.dropship) { + //the galaxy cargo bay is offset backwards from the center of the vehicle + carrier.Position + Vector3.Rz(Vector3(0, -7, 0), math.toRadians(carrier.Orientation.z)) + } else { + //the lodestar's cargo hold is almost the center of the vehicle + carrier.Position + } + val GUID0 = Service.defaultPlayerGUID + val zoneId = zone.id + val events = zone.VehicleEvents + val cargoActor = cargo.Actor + events ! VehicleServiceMessage( + s"$cargoActor", + VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health)) + ) + events ! VehicleServiceMessage( + s"$cargoActor", + VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) + ) + zone.actor ! ZoneActor.AddToBlockMap(cargo, carrier.Position) + if (carrier.isFlying) { + //the carrier vehicle is flying; eject the cargo vehicle + val ejectCargoMsg = + CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.InProgress, 0) + val detachCargoMsg = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition - Vector3.z(1), rotation) + val resetCargoMsg = + CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.Empty, 0) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, ejectCargoMsg)) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, detachCargoMsg)) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, resetCargoMsg)) + log.debug(s"HandleVehicleCargoDismount: eject - $ejectCargoMsg, detach - $detachCargoMsg") + if (driverOpt.isEmpty) { + //TODO cargo should drop like a rock like normal; until then, deconstruct it + cargoActor ! Vehicle.Deconstruct() + } + false + } else { + //the carrier vehicle is not flying; just open the door and let the cargo vehicle back out; force it out if necessary + val cargoStatusMessage = + CargoMountPointStatusMessage(carrierGUID, GUID0, cargoGUID, GUID0, mountPoint, CargoStatus.InProgress, 0) + val cargoDetachMessage = + ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition + Vector3.z(1f), rotation) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, cargoStatusMessage)) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, cargoDetachMessage)) + driverOpt match { + case Some(driver) => + events ! VehicleServiceMessage( + s"${driver.Name}", + VehicleAction.KickCargo(GUID0, cargo, cargo.Definition.AutoPilotSpeed2, 2500) + ) + case None => + val resetCargoMsg = + CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.Empty, 0) + events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, resetCargoMsg)) //lazy + //TODO cargo should back out like normal; until then, deconstruct it + cargoActor ! Vehicle.Deconstruct() + } + true + } + + case None => + log.warn(s"HandleDismountVehicleCargo: can not locate cargo $cargo in any hold of the carrier vehicle $carrier") + false + } + } + + //logging and messaging support functions + /** + * na + * @param decorator custom text for these messages in the log + * @param target an optional the target object + * @param targetGUID the expected globally unique identifier of the target object + */ + def LogCargoEventMissingVehicleError( + decorator: String, + target: Option[PlanetSideGameObject], + targetGUID: PlanetSideGUID + ): Unit = { + target match { + case Some(_: Vehicle) => ; + case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle") + case None => log.error(s"$decorator target $targetGUID has gone missing") + } + } + + /** + * Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + * that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @see `Vehicles.CargoOrientation` + * @param carrier the ferrying vehicle + * @param cargo the ferried vehicle + * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached; + * also known as a "cargo hold" + * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + */ + def CargoMountMessages( + carrier: Vehicle, + cargo: Vehicle, + mountPoint: Int + ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { + CargoMountMessages(carrier.GUID, cargo.GUID, mountPoint, Vehicles.CargoOrientation(cargo)) + } + + /** + * Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + * that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @param carrierGUID the ferrying vehicle + * @param cargoGUID the ferried vehicle + * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached + * @param orientation the positioning of the cargo vehicle in the carrier cargo bay + * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + */ + def CargoMountMessages( + carrierGUID: PlanetSideGUID, + cargoGUID: PlanetSideGUID, + mountPoint: Int, + orientation: Int + ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { + ( + ObjectAttachMessage(carrierGUID, cargoGUID, mountPoint), + CargoMountPointStatusMessage( + carrierGUID, + cargoGUID, + cargoGUID, + PlanetSideGUID(0), + mountPoint, + CargoStatus.Occupied, + orientation + ) + ) + } + + /** + * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @param carrier the ferrying vehicle + * @param cargo the ferried vehicle + * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached + * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + */ + def CargoMountBehaviorForOthers( + carrier: Vehicle, + cargo: Vehicle, + mountPoint: Int, + exclude: PlanetSideGUID + ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { + val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) + CargoMountMessagesForOthers(carrier.Zone, exclude, attachMessage, mountPointStatusMessage) + msgs + } + + /** + * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @param attachMessage an `ObjectAttachMessage` packet suitable for initializing cargo operations + * @param mountPointStatusMessage a `CargoMountPointStatusMessage` packet suitable for initializing cargo operations + */ + def CargoMountMessagesForOthers( + zone: Zone, + exclude: PlanetSideGUID, + attachMessage: ObjectAttachMessage, + mountPointStatusMessage: CargoMountPointStatusMessage + ): Unit = { + zone.VehicleEvents ! VehicleServiceMessage(zone.id, VehicleAction.SendResponse(exclude, attachMessage)) + zone.VehicleEvents ! VehicleServiceMessage(zone.id, VehicleAction.SendResponse(exclude, mountPointStatusMessage)) + } + + /** + * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to everyone. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @param carrier the ferrying vehicle + * @param cargo the ferried vehicle + * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached + * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + */ + def CargoMountBehaviorForAll( + carrier: Vehicle, + cargo: Vehicle, + mountPoint: Int + ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { + val zone = carrier.Zone + val zoneId = zone.id + val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) + zone.VehicleEvents ! VehicleServiceMessage( + zoneId, + VehicleAction.SendResponse(Service.defaultPlayerGUID, attachMessage) + ) + zone.VehicleEvents ! VehicleServiceMessage( + zoneId, + VehicleAction.SendResponse(Service.defaultPlayerGUID, mountPointStatusMessage) + ) + msgs + } +} + diff --git a/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala index ccfc6c3dd..0c6d30846 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala @@ -3,7 +3,7 @@ package net.psforever.objects.vehicles.control import net.psforever.objects._ import net.psforever.objects.serverobject.damage.{Damageable, DamageableVehicle} -import net.psforever.objects.vehicles.CargoBehavior +import net.psforever.objects.vehicles.{Cargo, CarrierBehavior} import net.psforever.objects.vital.interaction.DamageResult /** @@ -13,10 +13,15 @@ import net.psforever.objects.vital.interaction.DamageResult */ class CargoCarrierControl(vehicle: Vehicle) extends VehicleControl(vehicle) - with CargoBehavior { - def CargoObject = vehicle + with CarrierBehavior { + def CarrierObject = vehicle - override def commonEnabledBehavior: Receive = super.commonEnabledBehavior.orElse(cargoBehavior) + override def postStop() : Unit = { + super.postStop() + endAllCarrierOperations() + } + + override def commonEnabledBehavior: Receive = super.commonEnabledBehavior.orElse(carrierBehavior) /** * If the vehicle becomes disabled, the safety and autonomy of the cargo should be prioritized. @@ -24,21 +29,12 @@ class CargoCarrierControl(vehicle: Vehicle) */ override def PrepareForDisabled(kickPassengers: Boolean) : Unit = { //abandon all cargo - vehicle.CargoHolds.values - .collect { - case hold if hold.isOccupied => - val cargo = hold.occupant.get - CargoBehavior.HandleVehicleCargoDismount( - cargo.GUID, - cargo, - vehicle.GUID, - vehicle, - bailed = false, - requestedByPassenger = false, - kicked = false - ) - } - super.PrepareForDisabled(kickPassengers) + vehicle.CargoHolds.collect { + case (index, hold : Cargo) if hold.isOccupied => + val cargo = hold.occupant.get + checkCargoDismount(cargo.GUID, index, iteration = 0, bailed = false) + super.PrepareForDisabled(kickPassengers) + } } /** diff --git a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala index ef5014eea..2a05d9ec7 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala @@ -34,7 +34,7 @@ class DeployingVehicleControl(vehicle: Vehicle) case msg : Deployment.TryUndeploy => deployBehavior.apply(msg) - case msg @ Mountable.TryDismount(_, seat_num) => + case msg @ Mountable.TryDismount(_, seat_num, _) => dismountBehavior.apply(msg) dismountCleanup(seat_num) } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index 963d49846..6496d944e 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -18,7 +18,7 @@ import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.repair.RepairableVehicle import net.psforever.objects.serverobject.terminals.Terminal -import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior, Utility, VehicleLockState} +import net.psforever.objects.vehicles._ import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.VehicleShieldCharge import net.psforever.objects.vital.environment.EnvironmentReason @@ -51,7 +51,8 @@ class VehicleControl(vehicle: Vehicle) with JammableMountedWeapons with ContainableBehavior with AggravatedBehavior - with RespondsToZoneEnvironment { + with RespondsToZoneEnvironment + with CargoBehavior { //make control actors belonging to utilities when making control actor belonging to vehicle vehicle.Utilities.foreach { case (_, util) => util.Setup } @@ -68,6 +69,9 @@ class VehicleControl(vehicle: Vehicle) def ContainerObject = vehicle def InteractiveObject = vehicle + + def CargoObject = vehicle + SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) @@ -94,6 +98,7 @@ class VehicleControl(vehicle: Vehicle) util().Actor = Default.Actor } recoverFromEnvironmentInteracting() + endAllCargoOperations() } def commonEnabledBehavior: Receive = checkBehavior @@ -102,6 +107,7 @@ class VehicleControl(vehicle: Vehicle) .orElse(canBeRepairedByNanoDispenser) .orElse(containerBehavior) .orElse(environmentBehavior) + .orElse(cargoBehavior) .orElse { case Vehicle.Ownership(None) => LoseOwnership() @@ -113,7 +119,7 @@ class VehicleControl(vehicle: Vehicle) mountBehavior.apply(msg) mountCleanup(mount_point, player) - case msg @ Mountable.TryDismount(_, seat_num) => + case msg @ Mountable.TryDismount(_, seat_num, _) => dismountBehavior.apply(msg) dismountCleanup(seat_num) @@ -247,7 +253,7 @@ class VehicleControl(vehicle: Vehicle) def commonDisabledBehavior: Receive = checkBehavior .orElse { - case msg @ Mountable.TryDismount(_, seat_num) => + case msg @ Mountable.TryDismount(_, seat_num, _) => dismountBehavior.apply(msg) dismountCleanup(seat_num) @@ -372,13 +378,7 @@ class VehicleControl(vehicle: Vehicle) //escape being someone else's cargo vehicle.MountedIn match { case Some(_) => - CargoBehavior.HandleVehicleCargoDismount( - zone, - guid, - bailed = false, - requestedByPassenger = false, - kicked = false - ) + startCargoDismounting(bailed = false) case _ => ; } if (!vehicle.isFlying || kickPassengers) { @@ -386,13 +386,13 @@ class VehicleControl(vehicle: Vehicle) vehicle.Seats.values.foreach { seat => seat.occupant match { case Some(player) => - seat.unmount(player) + seat.unmount(player, BailType.Kicked) player.VehicleSeated = None if (player.isAlive) { zone.actor ! ZoneActor.AddToBlockMap(player, vehicle.Position) } if (player.HasGUID) { - events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, true, guid)) } case None => ; } diff --git a/src/main/scala/net/psforever/objects/vital/CollisionData.scala b/src/main/scala/net/psforever/objects/vital/CollisionData.scala new file mode 100644 index 000000000..50f9f6cbf --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/CollisionData.scala @@ -0,0 +1,78 @@ +//Copyright (c) 2021 PSForever +package net.psforever.objects.vital + +class CollisionData() { + var xy: CollisionXYData = CollisionXYData() + var z: CollisionZData = CollisionZData() +} + +class ExosuitCollisionData() { + var forceFactor: Float = 1f + var massFactor: Float = 1f +} + +class AdvancedCollisionData() extends CollisionData() { + var avatarCollisionDamageMax: Int = Int.MaxValue + + //I don't know what to do with these, so they will go here for now + var minHp: Float = 1f + var maxHp: Float = 10f + var minForce: Float = 15f + var maxForce: Float = 50f +} + +trait CollisionDoesDamage { + def hp(): List[Int] + + def hp(d: Int): Int = { + val _hp = hp() + _hp.lift(d) match { + case Some(n) => n + case None => _hp.last + } + } +} + +final case class CollisionZData(data: Iterable[(Float, Int)]) + extends CollisionDoesDamage { + assert(data.nonEmpty, "some collision data must be defined") + + def height(): List[Float] = data.unzip._1.toList + + override def hp(): List[Int] = data.unzip._2.toList + + def height(z: Float): Int = { + val n = data.toArray.indexWhere { case (h, _) => h > z } + if (n == -1) { + data.size - 1 + } else { + n + } + } +} + +object CollisionZData { + def apply(): CollisionZData = CollisionZData(Array((0f,0))) +} + +final case class CollisionXYData(data: Iterable[(Float, Int)]) + extends CollisionDoesDamage { + assert(data.nonEmpty, "some collision data must be defined") + + def throttle(): List[Float] = data.unzip._1.toList + + override def hp(): List[Int] = data.unzip._2.toList + + def throttle(z: Float): Int = { + val n = data.toArray.indexWhere { case (h, _) => h > z } + if (n == -1) { + data.size - 1 + } else { + n + } + } +} + +object CollisionXYData { + def apply(): CollisionXYData = CollisionXYData(Array((0f,0))) +} diff --git a/src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala b/src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala index 1f13f2fab..f3a41f11d 100644 --- a/src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala +++ b/src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala @@ -160,7 +160,7 @@ trait VitalityDefinition extends DamageModifiers { var explodes: Boolean = false /** - * damage that is inherent to the object, used for explosions and collisions, mainly + * damage that is inherent to the object, used for explosions, mainly */ var innateDamage: Option[DamageWithPosition] = None @@ -168,4 +168,8 @@ trait VitalityDefinition extends DamageModifiers { innateDamage = Some(combustion) innateDamage } + + val collision: CollisionData = new CollisionData() + + var mass: Float = 1f } diff --git a/src/main/scala/net/psforever/objects/vital/base/DamageResolution.scala b/src/main/scala/net/psforever/objects/vital/base/DamageResolution.scala index b38a92f1c..f47e2b4a3 100644 --- a/src/main/scala/net/psforever/objects/vital/base/DamageResolution.scala +++ b/src/main/scala/net/psforever/objects/vital/base/DamageResolution.scala @@ -30,6 +30,7 @@ object DamageResolution extends Enumeration { AggravatedSplashBurn, //continuous splashed aggravated damage Explosion, //area of effect damage caused by an internal mechanism; unrelated to Splash Environmental, //died to environmental causes - Suicide //i don't want to be the one the battles always choose + Suicide, //i don't want to be the one the battles always choose + Collision //went splat = Value } diff --git a/src/main/scala/net/psforever/objects/vital/collision/CollisionDamageModifierFunctions.scala b/src/main/scala/net/psforever/objects/vital/collision/CollisionDamageModifierFunctions.scala new file mode 100644 index 000000000..f3f66e9a3 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/collision/CollisionDamageModifierFunctions.scala @@ -0,0 +1,132 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vital.collision + +import net.psforever.objects.ballistics.{DeployableSource, PlayerSource, VehicleSource} +import net.psforever.objects.definition.ExoSuitDefinition +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.types.Vector3 + +/** + * Falling damage is a product of the falling distance. + */ +case object GroundImpact extends CollisionDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: CollisionReason): Int = + CollisionDamageModifierFunctions.calculateGroundImpact(damage, data, cause) +} + +/** + * Falling damage is a product of the falling distance. + */ +case object GroundImpactWith extends CollisionWithDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: CollisionWithReason): Int = + CollisionDamageModifierFunctions.calculateGroundImpact(damage, data, cause) +} + +/** + * The damage of a lateral collision is a product of how fast one is reported moving at the time of impact. + * As per the format, moving velocity is translated into a throttle gear related to maximum forward speed. + * Driving at high velocity into an inelastic structure is bad for one's integrity. + */ +case object HeadonImpact extends CollisionDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: CollisionReason): Int = { + val vel = Vector3.Magnitude(cause.velocity.xy) + if (vel > 0.05f) { + val definition = data.target.Definition + val xy = definition.collision.xy + damage + xy.hp(xy.throttle((vel + 0.5f) / definition.maxForwardSpeed)) + } else { + damage + } + } +} + +/** + * The damage of a lateral collision is a product of how fast one is reported moving at the time of impact. + * Vehicles colliding with infantry is a special case as vehicles have a canned amount of damage just for that target. + * Deployables might be rigged for instant destruction the moment vehicles collide with them; + * in any case, check the deployable for damage handling. + * For all other targets, e.g., vehicles against other vehicles, + * damage is a function of the velocity turned into a percentage of full throttle matched against tiers of damage. + */ +case object HeadonImpactWithEntity extends CollisionWithDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: CollisionWithReason): Int = { + val vel = Vector3.Magnitude(cause.velocity.xy) + (data.target, cause.collidedWith) match { + case (p: PlayerSource, v: VehicleSource) => + //player hit by vehicle; compromise between momentum-force-damage and velocity-damage + //the math here isn't perfect; 3D vector-velocity is being used in 1D momentum equations + val definition = v.Definition + val suit = ExoSuitDefinition.Select(p.ExoSuit, p.Faction).collision + val pmass = suit.massFactor + val pForceFactor = suit.forceFactor + val vmass = definition.mass + val maxAvtrDam = definition.collision.avatarCollisionDamageMax + val maxForwardSpeed = definition.maxForwardSpeed + val collisionTime = 1.5f //a drawn-out inelastic collision + val pvel = p.Velocity.getOrElse(Vector3.Zero) + val vvel = v.Velocity.getOrElse(Vector3.Zero) + val velCntrMass = (pvel * pmass + vvel * vmass) / (pmass + vmass) //velocity of the center of mass + val pvelFin = Vector3.neg(pvel - velCntrMass) + velCntrMass + val damp = math.min(pmass * Vector3.Magnitude(pvelFin - pvel) / (pForceFactor * collisionTime), maxAvtrDam.toFloat) + val dama = maxAvtrDam * 0.35f * (vel + 0.5f) / maxForwardSpeed + damage + (if (damp > dama) { + if (damp - dama > dama) { + damp - dama + } else { + dama + } + } else { + if (dama - damp > damp) { + dama - damp + } else { + damp + } + }).toInt + + case (a: DeployableSource, b) => + //deployable hit by vehicle; anything but an OHKO will cause visual desync, but still should calculate + val xy = a.Definition.collision.xy + damage + xy.hp(xy.throttle((vel + 0.5f) / b.Definition.maxForwardSpeed)) + case (_, b: VehicleSource) if vel > 0.05f => + //(usually) vehicle hit by another vehicle; exchange damages results + val xy = b.Definition.collision.xy + damage + xy.hp(xy.throttle((vel + 0.5f) / b.Definition.maxForwardSpeed)) + case (a, _) if vel > 0.05f => + //something hit by something + val xy = a.Definition.collision.xy + damage + xy.hp(xy.throttle((vel + 0.5f) / a.Definition.maxForwardSpeed)) + case _ => + //moving too slowly + damage + } + } +} + +/** + * When the target collides with something, + * if the target is not faction related with the cause, + * the target takes multiplied damage. + * The tactical resonance area protection is identified by never moving (has no velocity). + */ +case class TrapCollisionDamageMultiplier(multiplier: Float) extends CollisionWithDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: CollisionWithReason): Int = { + val target = data.target + if (target.Velocity.nonEmpty && target.Faction != cause.collidedWith.Faction) { + (multiplier * damage).toInt + } else { + damage + } + } +} + +object CollisionDamageModifierFunctions { + private[collision] def calculateGroundImpact(damage: Int, data: DamageInteraction, cause: CausedByColliding): Int = { + val fall = cause.fall + if (fall.toInt != 0) { + val z = data.target.Definition.collision.z + damage + z.hp(z.height(fall + 0.5f)) + } else { + damage + } + } +} diff --git a/src/main/scala/net/psforever/objects/vital/collision/CollisionDamageModifiers.scala b/src/main/scala/net/psforever/objects/vital/collision/CollisionDamageModifiers.scala new file mode 100644 index 000000000..cf89100e1 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/collision/CollisionDamageModifiers.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vital.collision + +import net.psforever.objects.vital.base._ +import net.psforever.objects.vital.interaction.DamageInteraction + +object CollisionDamageModifiers { + /** + * For modifiers that should be used with `CollisionReason`. + */ + trait Mod extends DamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int = { + cause match { + case o: CollisionReason => calculate(damage, data, o) + case _ => damage + } + } + + def calculate(damage: Int, data: DamageInteraction, cause: CollisionReason): Int + } +} + +object CollisionWithDamageModifiers { + /** + * or modifiers that should be used with `CollisionWithReason`. + */ + trait Mod extends DamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int = { + cause match { + case o: CollisionWithReason => calculate(damage, data, o) + case _ => damage + } + } + + def calculate(damage: Int, data: DamageInteraction, cause: CollisionWithReason): Int + } +} diff --git a/src/main/scala/net/psforever/objects/vital/collision/CollisionReason.scala b/src/main/scala/net/psforever/objects/vital/collision/CollisionReason.scala index e24fd14ec..7b45af571 100644 --- a/src/main/scala/net/psforever/objects/vital/collision/CollisionReason.scala +++ b/src/main/scala/net/psforever/objects/vital/collision/CollisionReason.scala @@ -1,38 +1,103 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.vital.collision -import net.psforever.objects.ballistics.SourceEntry -import net.psforever.objects.vital.base.{DamageReason, DamageResolution} +import net.psforever.objects.ballistics.{DeployableSource, SourceEntry, VehicleSource} +import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution, DamageType} import net.psforever.objects.vital.prop.DamageProperties import net.psforever.objects.vital.resolution.DamageAndResistance +import net.psforever.types.Vector3 /** - * A wrapper for a "damage source" in damage calculations - * that parameterizes information necessary to explain a collision. - * Being "adversarial" requires that the damage be performed as an aggressive action between individuals. - * @param source na + * Common base for reporting damage for reasons of collisions. */ -final case class AdversarialCollisionReason(source: DamageProperties) extends DamageReason { - def resolution: DamageResolution.Value = DamageResolution.Unresolved +trait CausedByColliding + extends DamageReason { + def resolution: DamageResolution.Value = DamageResolution.Collision - def same(test: DamageReason): Boolean = false + def source: DamageProperties = CollisionReason.noDamage - def damageModel: DamageAndResistance = null + def velocity: Vector3 - override def adversary : Option[SourceEntry] = None + def fall: Float } /** - * A wrapper for a "damage source" in damage calculations - * that parameterizes information necessary to explain a collision. - * @param source na + * A wrapper for a "damage source" in damage calculations that explains a collision. + * @param velocity how fast the target is moving prior to the collision + * @param fall ongoing vertical displacement since before the collision + * @param damageModel the functionality that is necessary for interaction + * of a vital game object with the rest of the hostile game world */ -final case class CollisionReason(source: DamageProperties) extends DamageReason { - def resolution: DamageResolution.Value = DamageResolution.Unresolved +final case class CollisionReason( + velocity: Vector3, + fall: Float, + damageModel: DamageAndResistance + ) extends CausedByColliding { + def same(test: DamageReason): Boolean = test match { + case cr: CollisionReason => cr.velocity == velocity && math.abs(cr.fall - fall) < 0.05f + case _ => false + } - def same(test: DamageReason): Boolean = false + override def adversary: Option[SourceEntry] = None - def damageModel: DamageAndResistance = null - - override def adversary : Option[SourceEntry] = None + override def unstructuredModifiers: List[DamageModifiers.Mod] = List( + GroundImpact, + HeadonImpact + ) +} + +/** + * A wrapper for a "damage source" in damage calculations that augment collision information + * by providing information about a qualified target that was struck. + * @param cause information about the collision + * @param collidedWith information regarding the qualified target that was struck + */ +final case class CollisionWithReason( + cause: CollisionReason, + collidedWith: SourceEntry + ) extends CausedByColliding { + def same(test: DamageReason): Boolean = test match { + case cr: CollisionWithReason => + cr.cause.same(cause) && cr.collidedWith == collidedWith + case _ => + false + } + + def velocity: Vector3 = cause.velocity + + def fall: Float = cause.fall + + def damageModel: DamageAndResistance = cause.damageModel + + override def adversary: Option[SourceEntry] = { + collidedWith match { + case v: VehicleSource => + v.occupants.head match { + case SourceEntry.None => Some(collidedWith) + case e => Some(e) + } + case d: DeployableSource => + d.owner match { + case SourceEntry.None => Some(collidedWith) + case e => Some(e) + } + case _ => + Some(collidedWith) + } + } + + override def unstructuredModifiers: List[DamageModifiers.Mod] = List( + GroundImpactWith, + HeadonImpactWithEntity + ) ++ collidedWith.Definition.Modifiers + + override def attribution : Int = collidedWith.Definition.ObjectId +} + +object CollisionReason { + /** The flags for calculating an absence of conventional damage for collision. + * Damage is considered `Direct`, however, which defines some resistance. */ + val noDamage = new DamageProperties { + CausesDamageType = DamageType.Direct + } } diff --git a/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index 63e34e28e..573fc6f83 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -5,6 +5,7 @@ import net.psforever.objects.serverobject.environment.PieceOfEnvironment import net.psforever.objects.serverobject.structures.FoundationBuilder import net.psforever.objects.serverobject.zipline.ZipLinePath import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObjectBuilder} +import net.psforever.types.Vector3 /** * The fixed instantiation and relation of a series of server objects.
@@ -129,4 +130,8 @@ class ZoneMap(val name: String) { lattice = lattice ++ Set((source, target)) } + def areValidCoordinates(coordinates: Vector3): Boolean = { + coordinates.x >= 0 && coordinates.x <= scale.width && + coordinates.y >= 0 && coordinates.y <= scale.height + } } diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala index 5aac419de..ca8506b3f 100644 --- a/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala +++ b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala @@ -230,7 +230,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) { def move(target: BlockMapEntity, toPosition: Vector3): SectorPopulation = { target.blockMapEntry match { case Some(entry) => move(target, toPosition, entry.coords, entry.range) - case None => SectorGroup(Nil) + case _ => SectorGroup(Nil) } } diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/BlockMapEntity.scala b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMapEntity.scala index 325a1fb53..88dd9f97a 100644 --- a/src/main/scala/net/psforever/objects/zones/blockmap/BlockMapEntity.scala +++ b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMapEntity.scala @@ -82,11 +82,15 @@ object BlockMapEntity { * To properly update the range, perform a proper update.) * @param target the entity on the blockmap * @param newCoords the world coordinates of the entity, the position to which it is moving / being moved - * @return always `true`; we are updating this entry + * @return `true`, if we are updating this entry; `false`, otherwsie */ private def updateBlockMap(target: BlockMapEntity, newCoords: Vector3): Boolean = { - val oldEntry = target.blockMapEntry.get - target.blockMapEntry = Some(BlockMapEntry(newCoords, oldEntry.range, oldEntry.sectors)) - true + target.blockMapEntry match { + case Some(oldEntry) => + target.blockMapEntry = Some(BlockMapEntry(newCoords, oldEntry.range, oldEntry.sectors)) + true + case None => + false + } } } diff --git a/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala b/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala index d6e4a2df3..b7cba00c3 100644 --- a/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala @@ -33,11 +33,11 @@ object DamageWithPositionMessage extends Marshallable[DamageWithPositionMessage] ).xmap[DamageWithPositionMessage] ( { case unk :: pos :: HNil => - DamageWithPositionMessage(math.max(0, math.min(unk, 255)), pos) + DamageWithPositionMessage(unk, pos) }, { case DamageWithPositionMessage(unk, pos) => - unk :: pos :: HNil + math.max(0, math.min(unk, 255)) :: pos :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/DismountVehicleCargoMsg.scala b/src/main/scala/net/psforever/packet/game/DismountVehicleCargoMsg.scala index 18817aaed..cdea289ff 100644 --- a/src/main/scala/net/psforever/packet/game/DismountVehicleCargoMsg.scala +++ b/src/main/scala/net/psforever/packet/game/DismountVehicleCargoMsg.scala @@ -7,12 +7,15 @@ import scodec.Codec import scodec.codecs._ /** - * Note: For some reason this packet does not include the GUID of the vehicle that is being dismounted from. As a workaround vehicle.MountedIn in manually set/removed - * @param player_guid // GUID of the player that is rqeuesting dismount - * @param vehicle_guid GUID of the vehicle that is requesting dismount - * @param bailed If the vehicle bailed out of the cargo vehicle - * @param requestedByPassenger If a passenger of the vehicle in the cargo bay requests dismount this bit will be set - * @param kicked If the vehicle was kicked by the cargo vehicle pilot + * Request dismount of one vehicle (cargo) that is being ferried by another vehicle (carrier). + * The carrier has what is called a "cargo bay" which is where the cargo is being stored for ferrying. + * @param player_guid GUID of the player that is rqeuesting dismount; + * when kicked by carrier driver, player_guid will be PlanetSideGUID(0); + * when exiting of the cargo vehicle driver's own accord, player_guid will be the cargo vehicle driver + * @param vehicle_guid GUID of the vehicle that is requesting dismount (cargo) + * @param bailed if the cargo vehicle bailed out of the cargo vehicle + * @param requestedByPassenger if a passenger of the cargo vehicle requests dismount + * @param kicked if the cargo vehicle was kicked by the cargo vehicle pilot */ final case class DismountVehicleCargoMsg( player_guid: PlanetSideGUID, diff --git a/src/main/scala/net/psforever/packet/game/GenericCollisionMsg.scala b/src/main/scala/net/psforever/packet/game/GenericCollisionMsg.scala index 1e0046b36..57acd3033 100644 --- a/src/main/scala/net/psforever/packet/game/GenericCollisionMsg.scala +++ b/src/main/scala/net/psforever/packet/game/GenericCollisionMsg.scala @@ -1,10 +1,27 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game -import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import enumeratum.values.{IntEnum, IntEnumEntry} +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.Attempt.Successful import scodec.Codec import scodec.codecs._ +import shapeless.{::, HNil} + +sealed abstract class CollisionIs(val value: Int) extends IntEnumEntry + +object CollisionIs extends IntEnum[CollisionIs] { + val values = findValues + + case object OfGroundVehicle extends CollisionIs(value = 0) + + case object OfAircraft extends CollisionIs(value = 1) + + case object OfInfantry extends CollisionIs(value = 2) + + case object BetweenThings extends CollisionIs(value = 3) //currently, invalid +} /** * Dispatched by the client when the player has encountered a physical interaction that would cause damage.
@@ -13,7 +30,7 @@ import scodec.codecs._ * The first is the `player`, that is, the client's avatar. * The second is the `target` with respect to the `player` - whatever the avatar ran into, or whatever ran into the avatar. * In the case of isolated forms of collision such as fall damage the `target` fields are blank or zero'd. - * @param unk1 na + * @param collision_type a brief hint at the sort of interaction * @param player the player or player-controlled vehicle * @param target the other party in the collision * @param player_health the player's health @@ -22,42 +39,56 @@ import scodec.codecs._ * @param target_velocity the target's velocity * @param player_pos the player's world coordinates * @param target_pos the target's world coordinates + * @param unk1 na * @param unk2 na * @param unk3 na - * @param unk4 na */ final case class GenericCollisionMsg( - unk1: Int, - player: PlanetSideGUID, - target: PlanetSideGUID, - player_health: Int, - target_health: Int, - player_velocity: Vector3, - target_velocity: Vector3, - player_pos: Vector3, - target_pos: Vector3, - unk2: Long, - unk3: Long, - unk4: Long -) extends PlanetSideGamePacket { + collision_type: CollisionIs, + player: PlanetSideGUID, + player_health: Int, + player_pos: Vector3, + player_velocity: Vector3, + target: PlanetSideGUID, + target_health: Int, + target_pos: Vector3, + target_velocity: Vector3, + unk1: Long, + unk2: Long, + unk3: Long + ) extends PlanetSideGamePacket { type Packet = GenericCollisionMsg def opcode = GamePacketOpcode.GenericCollisionMsg def encode = GenericCollisionMsg.encode(this) } object GenericCollisionMsg extends Marshallable[GenericCollisionMsg] { + private val collisionIsCodec: Codec[CollisionIs] = PacketHelpers.createIntEnumCodec(CollisionIs, uint2) + + private val velocityFloatCodec: Codec[Vector3] = Vector3.codec_float + .narrow[Vector3](a => Successful(a * 3.6f), a => a * 0.2777778f) //this precision is necessary + implicit val codec: Codec[GenericCollisionMsg] = ( - ("unk1" | uint2) :: - ("p" | PlanetSideGUID.codec) :: - ("t" | PlanetSideGUID.codec) :: - ("p_health" | uint16L) :: - ("t_health" | uint16L) :: - ("p_vel" | Vector3.codec_float) :: - ("t_vel" | Vector3.codec_float) :: - ("p_pos" | Vector3.codec_pos) :: - ("t_pos" | Vector3.codec_pos) :: - ("unk2" | uint32L) :: - ("unk3" | uint32L) :: - ("unk4" | uint32L) - ).as[GenericCollisionMsg] + ("collision_type" | collisionIsCodec) :: + ("p" | PlanetSideGUID.codec) :: + ("t" | PlanetSideGUID.codec) :: + ("p_health" | uint16L) :: + ("t_health" | uint16L) :: + ("p_vel" | velocityFloatCodec) :: + ("t_vel" | velocityFloatCodec) :: + ("p_pos" | Vector3.codec_pos) :: + ("t_pos" | Vector3.codec_pos) :: + ("unk1" | uint32L) :: + ("unk2" | uint32L) :: + ("unk3" | uint32L) + ).xmap[GenericCollisionMsg]( + { + case ct :: p :: t :: ph :: th :: pv :: tv :: pp :: tp :: u1 :: u2 :: u3 :: HNil => + GenericCollisionMsg(ct, p, ph, pp, pv, t, th, tp, tv, u1, u2, u3) + }, + { + case GenericCollisionMsg(ct, p, ph, pp, pv, t, th, tp, tv, u1, u2, u3) => + ct :: p :: t :: ph :: th :: pv :: tv :: pp :: tp :: u1 :: u2 :: u3 :: HNil + } + ) } diff --git a/src/main/scala/net/psforever/packet/game/MountVehicleCargoMsg.scala b/src/main/scala/net/psforever/packet/game/MountVehicleCargoMsg.scala index 91649384f..79733d75b 100644 --- a/src/main/scala/net/psforever/packet/game/MountVehicleCargoMsg.scala +++ b/src/main/scala/net/psforever/packet/game/MountVehicleCargoMsg.scala @@ -10,7 +10,7 @@ import scodec.codecs._ * @param player_guid The guid of the player sending the request to board another vehicle with a cargo vehicle * @param vehicle_guid The guid of the vehicle for the requesting player * @param target_vehicle The cargo vehicle guid e.g. Galaxy / Lodestar - * @param unk4 + * @param unk4 na */ final case class MountVehicleCargoMsg( player_guid: PlanetSideGUID, diff --git a/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala b/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala index a00d1ff82..06bec7409 100644 --- a/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala @@ -20,7 +20,9 @@ import scodec.codecs._ * in repeating order: 13, 14, 10, 11, 12, 15; * `None`, when landed and for all vehicles that do not fly * @param unk3 na - * @param unk4 na + * @param unk4 na; + * 0 by default; + * for mosquito, can become various numbers during collision damage * @param wheel_direction for ground vehicles, whether and how much the wheels are being turned; * 15 for straight; * 0 for hard right; @@ -80,15 +82,15 @@ object VehicleStateMessage extends Marshallable[VehicleStateMessage] { implicit val codec: Codec[VehicleStateMessage] = ( ("vehicle_guid" | PlanetSideGUID.codec) :: - ("unk1" | uintL(3)) :: - ("pos" | Vector3.codec_pos) :: - ("ang" | codec_orient) :: - optional(bool, "vel" | Vector3.codec_vel) :: - optional(bool, "flying" | uintL(5)) :: - ("unk3" | uintL(7)) :: - ("unk4" | uint4L) :: - ("wheel_direction" | uintL(5)) :: - ("int5" | bool) :: - ("is_cloaked" | bool) + ("unk1" | uintL(bits = 3)) :: + ("pos" | Vector3.codec_pos) :: + ("ang" | codec_orient) :: + ("vel" | optional(bool, Vector3.codec_vel)) :: + ("flying" | optional(bool, uintL(bits = 5))) :: + ("unk3" | uintL(bits = 7)) :: + ("unk4" | uint4L) :: + ("wheel_direction" | uintL(5)) :: + ("is_decelerating" | bool) :: + ("is_cloaked" | bool) ).as[VehicleStateMessage] } diff --git a/src/main/scala/net/psforever/types/Vector3.scala b/src/main/scala/net/psforever/types/Vector3.scala index ea821ce06..6c604d257 100644 --- a/src/main/scala/net/psforever/types/Vector3.scala +++ b/src/main/scala/net/psforever/types/Vector3.scala @@ -2,6 +2,7 @@ package net.psforever.types import net.psforever.newcodecs._ +import scodec.Attempt.Successful import scodec.Codec import scodec.codecs._ @@ -38,6 +39,18 @@ final case class Vector3(x: Float, y: Float, z: Float) { Vector3(x * scalar, y * scalar, z * scalar) } + /** + * Operator for vector scaling, treating `Vector3` objects as actual mathematical vectors. + * The application of this overload is "vector / scalar" exclusively. + * "scalar / vector" is invalid. + * Due to rounding, may not be perfectly equivalent to "vector * ( 1 / scalar )". + * @param scalar the value to divide this vector + * @return a new `Vector3` object + */ + def /(scalar: Float): Vector3 = { + Vector3(x / scalar, y / scalar, z / scalar) + } + /** * Operator for returning the ground-planar coordinates * and ignoring the perpendicular distance from the world floor. @@ -100,7 +113,7 @@ object Vector3 { ("x" | newcodecs.q_float(-256.0, 256.0, 14)) :: ("y" | newcodecs.q_float(-256.0, 256.0, 14)) :: ("z" | newcodecs.q_float(-256.0, 256.0, 14)) - ).as[Vector3] + ).as[Vector3].narrow(a => Successful(a * 3.6f), a => a * 0.2778f) implicit val codec_float: Codec[Vector3] = ( ("x" | floatL) :: diff --git a/src/test/scala/CodecTest.scala b/src/test/scala/CodecTest.scala index fd1a25dda..c49b836e2 100644 --- a/src/test/scala/CodecTest.scala +++ b/src/test/scala/CodecTest.scala @@ -50,11 +50,11 @@ class CodecTest extends Specification { val string_vel = hex"857D4E0FFFC0" "decode" in { - Vector3.codec_vel.decode(string_vel.bits).require.value mustEqual Vector3(-3.84375f, 2.59375f, 255.96875f) + Vector3.codec_vel.decode(string_vel.bits).require.value mustEqual Vector3(-13.8375f, 9.3375f, 921.4875f) } "encode" in { - Vector3.codec_vel.encode(Vector3(-3.84375f, 2.59375f, 255.96875f)).require.bytes mustEqual string_vel + Vector3.codec_vel.encode(Vector3(-13.8375f, 9.3375f, 921.4875f)).require.bytes mustEqual string_vel } } } diff --git a/src/test/scala/game/FireHintMessageTest.scala b/src/test/scala/game/FireHintMessageTest.scala index 4eeb1c8bd..868b454b8 100644 --- a/src/test/scala/game/FireHintMessageTest.scala +++ b/src/test/scala/game/FireHintMessageTest.scala @@ -20,7 +20,7 @@ class FireHintMessageTest extends Specification { u2 mustEqual 65399 u3 mustEqual 7581 u4 mustEqual 0 - u5 mustEqual None + u5.isEmpty mustEqual true case _ => ko } @@ -34,7 +34,7 @@ class FireHintMessageTest extends Specification { u2 mustEqual 65231 u3 mustEqual 64736 u4 mustEqual 3 - u5 mustEqual Some(Vector3(21.5f, -6.8125f, 2.65625f)) + u5.contains(Vector3(77.4f, -24.525f, 9.5625f)) mustEqual true case _ => ko } @@ -46,7 +46,7 @@ class FireHintMessageTest extends Specification { pkt mustEqual string } - "encode string2" in { + "encode (string2)" in { val msg = FireHintMessage( PlanetSideGUID(3592), Vector3(2910.789f, 3744.875f, 69.0625f), @@ -54,7 +54,7 @@ class FireHintMessageTest extends Specification { 65231, 64736, 3, - Some(Vector3(21.5f, -6.8125f, 2.65625f)) + Some(Vector3(77.4f, -24.525f, 9.5625f)) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector diff --git a/src/test/scala/game/GenericCollisionMsgTest.scala b/src/test/scala/game/GenericCollisionMsgTest.scala index dca780451..7ef72aa96 100644 --- a/src/test/scala/game/GenericCollisionMsgTest.scala +++ b/src/test/scala/game/GenericCollisionMsgTest.scala @@ -11,17 +11,18 @@ class GenericCollisionMsgTest extends Specification { //TODO find a better test later val string = hex"3C 92C00000190000001B2A8010932CEF505C70946F00000000000000000000000017725EBC6D6A058000000000000000000000000000003F8FF45140" + "decode" in { PacketCoding.decodePacket(string).require match { - case GenericCollisionMsg(unk1, p, t, php, thp, pv, tv, ppos, tpos, unk2, unk3, unk4) => - unk1 mustEqual 2 + case GenericCollisionMsg(ct, p, php, ppos, pv, t, thp, tpos, tv, unk2, unk3, unk4) => + ct mustEqual CollisionIs.OfInfantry p mustEqual PlanetSideGUID(75) t mustEqual PlanetSideGUID(0) php mustEqual 100 thp mustEqual 0 - pv.x mustEqual 32.166428f - pv.y mustEqual 23.712547f - pv.z mustEqual -0.012802706f + pv.x mustEqual 115.79913f + pv.y mustEqual 85.365166f + pv.z mustEqual -0.046089742f tv.x mustEqual 0.0f tv.z mustEqual 0.0f tv.x mustEqual 0.0f @@ -38,22 +39,48 @@ class GenericCollisionMsgTest extends Specification { ko } } + "encode" in { val msg = GenericCollisionMsg( - 2, + CollisionIs.OfInfantry, PlanetSideGUID(75), - PlanetSideGUID(0), 100, - 0, - Vector3(32.166428f, 23.712547f, -0.012802706f), - Vector3(0.0f, 0.0f, 0.0f), Vector3(3986.7266f, 2615.3672f, 90.625f), + Vector3(115.79913f, 85.365166f, -0.046089742f), + PlanetSideGUID(0), + 0, + Vector3(0.0f, 0.0f, 0.0f), Vector3(0.0f, 0.0f, 0.0f), 0L, 0L, 1171341310L ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector - pkt mustEqual string + //pkt mustEqual string + PacketCoding.decodePacket(pkt).require match { + case GenericCollisionMsg(ct, p, php, ppos, pv, t, thp, tpos, tv, unk2, unk3, unk4) => + ct mustEqual CollisionIs.OfInfantry + p mustEqual PlanetSideGUID(75) + t mustEqual PlanetSideGUID(0) + php mustEqual 100 + thp mustEqual 0 + pv.x mustEqual 115.79913f + pv.y mustEqual 85.365166f + pv.z mustEqual -0.046089742f + tv.x mustEqual 0.0f + tv.z mustEqual 0.0f + tv.x mustEqual 0.0f + ppos.x mustEqual 3986.7266f + ppos.y mustEqual 2615.3672f + ppos.z mustEqual 90.625f + tpos.x mustEqual 0.0f + tpos.y mustEqual 0.0f + tpos.z mustEqual 0.0f + unk2 mustEqual 0L + unk3 mustEqual 0L + unk4 mustEqual 1171341310L + case _ => + ko + } } } diff --git a/src/test/scala/game/LongRangeProjectileInfoMessageTest.scala b/src/test/scala/game/LongRangeProjectileInfoMessageTest.scala index 250f6db1f..ca89aaef3 100644 --- a/src/test/scala/game/LongRangeProjectileInfoMessageTest.scala +++ b/src/test/scala/game/LongRangeProjectileInfoMessageTest.scala @@ -15,7 +15,7 @@ class LongRangeProjectileInfoMessageTest extends Specification { case LongRangeProjectileInfoMessage(guid, pos, vel) => guid mustEqual PlanetSideGUID(5330) pos mustEqual Vector3(2264, 5115.039f, 31.046875f) - vel.contains(Vector3(-57.1875f, 9.875f, 47.5f)) mustEqual true + vel.contains(Vector3(-205.875f, 35.55f, 171.0f)) mustEqual true case _ => ko } @@ -25,7 +25,7 @@ class LongRangeProjectileInfoMessageTest extends Specification { val msg = LongRangeProjectileInfoMessage( PlanetSideGUID(5330), Vector3(2264, 5115.039f, 31.046875f), - Vector3(-57.1875f, 9.875f, 47.5f) + Vector3(-205.875f, 35.55f, 171.0f) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string diff --git a/src/test/scala/game/PlayerStateMessageTest.scala b/src/test/scala/game/PlayerStateMessageTest.scala index 35c5c6021..d33568985 100644 --- a/src/test/scala/game/PlayerStateMessageTest.scala +++ b/src/test/scala/game/PlayerStateMessageTest.scala @@ -99,8 +99,8 @@ class PlayerStateMessageTest extends Specification { pos.y mustEqual 5987.6016f pos.z mustEqual 44.1875f vel.isDefined mustEqual true - vel.get.x mustEqual 2.53125f - vel.get.y mustEqual 6.5625f + vel.get.x mustEqual 9.1125f + vel.get.y mustEqual 23.625f vel.get.z mustEqual 0.0f facingYaw mustEqual 22.5f facingPitch mustEqual -11.25f @@ -155,7 +155,7 @@ class PlayerStateMessageTest extends Specification { val msg = PlayerStateMessage( PlanetSideGUID(1696), Vector3(4008.6016f, 5987.6016f, 44.1875f), - Some(Vector3(2.53125f, 6.5625f, 0f)), + Some(Vector3(9.1125f, 23.625f, 0f)), 22.5f, -11.25f, 0f, diff --git a/src/test/scala/game/PlayerStateMessageUpstreamTest.scala b/src/test/scala/game/PlayerStateMessageUpstreamTest.scala index 8598e385b..e3989d8b0 100644 --- a/src/test/scala/game/PlayerStateMessageUpstreamTest.scala +++ b/src/test/scala/game/PlayerStateMessageUpstreamTest.scala @@ -30,7 +30,7 @@ class PlayerStateMessageUpstreamTest extends Specification { ) => avatar_guid mustEqual PlanetSideGUID(75) pos mustEqual Vector3(3694.1094f, 2735.4531f, 90.84375f) - vel.contains(Vector3(4.375f, 2.59375f, 0.0f)) mustEqual true + vel.contains(Vector3(15.75f, 9.3375f, 0.0f)) mustEqual true facingYaw mustEqual 61.875f facingPitch mustEqual -8.4375f facingYawUpper mustEqual 0.0f @@ -51,7 +51,7 @@ class PlayerStateMessageUpstreamTest extends Specification { val msg = PlayerStateMessageUpstream( PlanetSideGUID(75), Vector3(3694.1094f, 2735.4531f, 90.84375f), - Some(Vector3(4.375f, 2.59375f, 0.0f)), + Some(Vector3(15.75f, 9.3375f, 0.0f)), 61.875f, -8.4375f, 0.0f, diff --git a/src/test/scala/game/PlayerStateShiftMessageTest.scala b/src/test/scala/game/PlayerStateShiftMessageTest.scala index dd3043ff1..d37915963 100644 --- a/src/test/scala/game/PlayerStateShiftMessageTest.scala +++ b/src/test/scala/game/PlayerStateShiftMessageTest.scala @@ -49,9 +49,9 @@ class PlayerStateShiftMessageTest extends Specification { state.get.pos.z mustEqual 50.3125f state.get.viewYawLim mustEqual 50.625f state.get.vel.isDefined mustEqual true - state.get.vel.get.x mustEqual 2.8125f - state.get.vel.get.y mustEqual -8.0f - state.get.vel.get.z mustEqual 0.375f + state.get.vel.get.x mustEqual 10.125f + state.get.vel.get.y mustEqual -28.8f + state.get.vel.get.z mustEqual 1.3499999f unk.isDefined mustEqual false case _ => ko @@ -74,7 +74,7 @@ class PlayerStateShiftMessageTest extends Specification { "encode (pos and vel)" in { val msg = PlayerStateShiftMessage( - ShiftState(2, Vector3(4645.75f, 5811.6016f, 50.3125f), 50.625f, Vector3(2.8125f, -8.0f, 0.375f)) + ShiftState(2, Vector3(4645.75f, 5811.6016f, 50.3125f), 50.625f, Vector3(10.125f, -28.8f, 1.3499999f)) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector diff --git a/src/test/scala/game/SplashHitMessageTest.scala b/src/test/scala/game/SplashHitMessageTest.scala index 4c60fc922..5d3ec0392 100644 --- a/src/test/scala/game/SplashHitMessageTest.scala +++ b/src/test/scala/game/SplashHitMessageTest.scala @@ -21,10 +21,7 @@ class SplashHitMessageTest extends Specification { projectile_pos.z mustEqual 90.921875f unk2 mustEqual 0 unk3 mustEqual 0 - projectile_vel.isDefined mustEqual true - projectile_vel.get.x mustEqual 2.21875f - projectile_vel.get.y mustEqual 0.90625f - projectile_vel.get.z mustEqual -1.125f + projectile_vel.contains(Vector3(7.9874997f, 3.2624998f, -4.0499997f)) mustEqual true unk4.isDefined mustEqual false targets.size mustEqual 2 //0 @@ -53,7 +50,7 @@ class SplashHitMessageTest extends Specification { Vector3(3681.3438f, 2728.9062f, 90.921875f), 0, 0, - Some(Vector3(2.21875f, 0.90625f, -1.125f)), + Some(Vector3(7.9874997f, 3.2624998f, -4.0499997f)), None, SplashedTarget(PlanetSideGUID(75), Vector3(3674.8438f, 2726.789f, 91.15625f), 286326784L, None) :: SplashedTarget(PlanetSideGUID(372), Vector3(3679.1328f, 2722.6016f, 92.765625f), 268435456L, None) :: diff --git a/src/test/scala/game/VehicleStateMessageTest.scala b/src/test/scala/game/VehicleStateMessageTest.scala index f6e9045df..3b23700f0 100644 --- a/src/test/scala/game/VehicleStateMessageTest.scala +++ b/src/test/scala/game/VehicleStateMessageTest.scala @@ -24,7 +24,7 @@ class VehicleStateMessageTest extends Specification { vel.isDefined mustEqual true vel.get.x mustEqual 0.0f vel.get.y mustEqual 0.0f - vel.get.z mustEqual 0.03125f + vel.get.z mustEqual 0.1125f unk2.isDefined mustEqual false unk3 mustEqual 0 unk4 mustEqual 0 @@ -42,7 +42,7 @@ class VehicleStateMessageTest extends Specification { 0, Vector3(3674.8438f, 2726.789f, 91.09375f), Vector3(359.29688f, 1.0546875f, 90.35156f), - Some(Vector3(0.0f, 0.0f, 0.03125f)), + Some(Vector3(0.0f, 0.0f, 0.1125f)), None, 0, 0, diff --git a/src/test/scala/game/VehicleSubStateMessageTest.scala b/src/test/scala/game/VehicleSubStateMessageTest.scala index c84d1ead1..26728c31f 100644 --- a/src/test/scala/game/VehicleSubStateMessageTest.scala +++ b/src/test/scala/game/VehicleSubStateMessageTest.scala @@ -18,7 +18,7 @@ class VehicleSubStateMessageTest extends Specification { vehicle_pos mustEqual Vector3(3465.9575f, 2873.3635f, 91.05253f) vehicle_ang mustEqual Vector3(11.6015625f, 0.0f, 3.515625f) vel.isDefined mustEqual true - vel.get mustEqual Vector3(-0.40625f, 0.03125f, -0.8125f) + vel.contains(Vector3(-1.4625f, 0.1125f, -2.925f)) mustEqual true unk1 mustEqual false unk2.isDefined mustEqual false case _ => @@ -32,7 +32,7 @@ class VehicleSubStateMessageTest extends Specification { PlanetSideGUID(3376), Vector3(3465.9575f, 2873.3635f, 91.05253f), Vector3(11.6015625f, 0.0f, 3.515625f), - Some(Vector3(-0.40625f, 0.03125f, -0.8125f)), + Some(Vector3(-1.4625f, 0.1125f, -2.925f)), false, None ) diff --git a/src/test/scala/game/objectcreate/CharacterDataTest.scala b/src/test/scala/game/objectcreate/CharacterDataTest.scala index 82a7b3760..81e01df66 100644 --- a/src/test/scala/game/objectcreate/CharacterDataTest.scala +++ b/src/test/scala/game/objectcreate/CharacterDataTest.scala @@ -34,7 +34,7 @@ class CharacterDataTest extends Specification { pos.coord mustEqual Vector3(3674.8438f, 2726.789f, 91.15625f) pos.orient mustEqual Vector3(0f, 0f, 64.6875f) pos.vel.isDefined mustEqual true - pos.vel.get mustEqual Vector3(1.4375f, -0.4375f, 0f) + pos.vel.get mustEqual Vector3(5.1749997f, -1.5749999f, 0.0f) basic match { case CharacterAppearanceData(a, b, ribbons) => @@ -294,7 +294,7 @@ class CharacterDataTest extends Specification { val pos: PlacementData = PlacementData( Vector3(3674.8438f, 2726.789f, 91.15625f), Vector3(0f, 0f, 64.6875f), - Some(Vector3(1.4375f, -0.4375f, 0f)) + Some(Vector3(5.1749997f, -1.5749999f, 0.0f)) ) val a: Int => CharacterAppearanceA = CharacterAppearanceA( BasicCharacterData( diff --git a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala index 9a85310d8..c592df7d6 100644 --- a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala +++ b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala @@ -28,7 +28,7 @@ class MountedVehiclesTest extends Specification { case vdata: VehicleData => vdata.pos.coord mustEqual Vector3(4571.6875f, 5602.1875f, 93) vdata.pos.orient mustEqual Vector3(11.25f, 2.8125f, 92.8125f) - vdata.pos.vel.contains(Vector3(31.71875f, 8.875f, -0.03125f)) mustEqual true + vdata.pos.vel.contains(Vector3(114.1875f, 31.949999f, -0.1125f)) mustEqual true vdata.data.faction mustEqual PlanetSideEmpire.TR vdata.data.bops mustEqual false vdata.data.alternate mustEqual false @@ -264,7 +264,7 @@ class MountedVehiclesTest extends Specification { PlacementData( Vector3(4571.6875f, 5602.1875f, 93), Vector3(11.25f, 2.8125f, 92.8125f), - Some(Vector3(31.71875f, 8.875f, -0.03125f)) + Some(Vector3(114.1875f, 31.949999f, -0.1125f)) ), CommonFieldData(PlanetSideEmpire.TR, false, false, false, None, false, Some(false), None, PlanetSideGUID(3776)), false, diff --git a/src/test/scala/objects/DamageableTest.scala b/src/test/scala/objects/DamageableTest.scala index 8c2719c48..aa4f6c256 100644 --- a/src/test/scala/objects/DamageableTest.scala +++ b/src/test/scala/objects/DamageableTest.scala @@ -1061,7 +1061,7 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { assert(!turret.Destroyed) turret.Actor ! Vitality.Damage(applyDamageToA) //also test destruction while jammered - vehicleProbe.receiveN(2, 1000 milliseconds) //flush jammered messages (see above) + vehicleProbe.receiveN(2, 1000 milliseconds) //flush jammered messages (see above) assert(turret.Health > turret.Definition.DamageDestroysAt) assert(turret.Jammed) assert(!turret.Destroyed) @@ -1071,44 +1071,37 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { player1Probe.expectNoMessage(500 milliseconds) val msg3 = player2Probe.receiveOne(200 milliseconds) val msg56 = vehicleProbe.receiveN(2, 200 milliseconds) - assert( - msg12_4.head match { - case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true - case _ => false - } - ) - assert( - msg12_4(1) match { - case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, _, Vector3(1, 0, 0))) => true - case _ => false - } - ) - assert( - msg3 match { - case Player.Die(_) => true - case _ => false - } - ) - assert( - msg12_4(2) match { - case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(0), PlanetSideGUID(5), _)) => true - case _ => false - } - ) - assert( - msg56.head match { - case VehicleServiceMessage.TurretUpgrade(SupportActor.ClearSpecific(List(t), _)) if turret eq t => true - case _ => false - } - ) - assert( - msg56(1) match { - case VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(t, _, TurretUpgrade.None, _)) - if t eq turret => - true - case _ => false - } - ) + msg12_4.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => ; + case _ => + assert(false, s"DamageableWeaponTurretDestructionTest-1: ${msg12_4.head}") + } + msg12_4(1) match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, _, Vector3(1, 0, 0))) => ; + case _ => + assert(false, s"DamageableWeaponTurretDestructionTest-2: ${msg12_4(1)}") + } + msg3 match { + case Player.Die(_) => true + case _ => + assert(false, s"DamageableWeaponTurretDestructionTest-3: player not dead - $msg3") + } + msg12_4(2) match { + case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(0), PlanetSideGUID(5), _)) => ; + case _ => + assert(false, s"DamageableWeaponTurretDestructionTest-4: ${msg12_4(2)}") + } + msg56.head match { + case VehicleServiceMessage.TurretUpgrade(SupportActor.ClearSpecific(List(t), _)) if turret eq t => ; + case _ => + assert(false, s"DamageableWeaponTurretDestructionTest-5: ${msg56.head}") + } + msg56(1) match { + case VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(t, _, TurretUpgrade.None, _)) if t eq turret => ; + true + case _ => + assert(false, s"DamageableWeaponTurretDestructionTest-6: ${msg56(1)}") + } assert(turret.Health <= turret.Definition.DamageDestroysAt) assert(!turret.Jammed) assert(turret.Destroyed) diff --git a/src/test/scala/objects/VehicleControlTest.scala b/src/test/scala/objects/VehicleControlTest.scala index 43645b8fb..e06c4a97f 100644 --- a/src/test/scala/objects/VehicleControlTest.scala +++ b/src/test/scala/objects/VehicleControlTest.scala @@ -14,7 +14,7 @@ import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles.VehicleLockState -import net.psforever.objects.vehicles.control.VehicleControl +import net.psforever.objects.vehicles.control.{CargoCarrierControl, VehicleControl} import net.psforever.objects.vital.{VehicleShieldCharge, Vitality} import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.packet.game._ @@ -66,16 +66,11 @@ class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { vehicle.Actor ! Vehicle.Deconstruct() val vehicle_msg = vehicleProbe.receiveN(1, 500 milliseconds) - assert( - vehicle_msg.head match { - case VehicleServiceMessage( - "test", - VehicleAction.KickPassenger(PlanetSideGUID(2), 4, false, PlanetSideGUID(1)) - ) => - true - case _ => false - } - ) + vehicle_msg.head match { + case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(2), 4, true, PlanetSideGUID(1))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionPassengerTest: ${vehicle_msg.head}") + } assert(player1.VehicleSeated.isEmpty) assert(vehicle.Seats(1).occupant.isEmpty) } @@ -96,19 +91,22 @@ class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTes val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) vehicle.Faction = PlanetSideEmpire.TR - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test-cargo") vehicle.Zone = zone val lodestar = Vehicle(GlobalDefinitions.lodestar) lodestar.Faction = PlanetSideEmpire.TR + lodestar.Zone = zone val player1 = Player(VehicleTest.avatar1) //name="test1" val player2 = Player(VehicleTest.avatar2) //name="test2" guid.register(vehicle, 1) guid.register(lodestar, 2) - player1.GUID = PlanetSideGUID(3) - var utilityId = 10 + guid.register(player1, 3) + guid.register(player2, 4) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test-cargo") + lodestar.Actor = system.actorOf(Props(classOf[CargoCarrierControl], lodestar), "vehicle-test-carrier") + var utilityId = 5 lodestar.Utilities.values.foreach { util => - util().GUID = PlanetSideGUID(utilityId) + guid.register(util(), utilityId) utilityId += 1 } vehicle.Seats(1).mount(player1) //passenger mount @@ -123,81 +121,49 @@ class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTes zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this "VehicleControl" should { - "if mounted as cargo, self-eject when marked for deconstruction" in { + "self-eject when marked for deconstruction if mounted as cargo" in { + assert(player1.VehicleSeated.nonEmpty) + assert(vehicle.Seats(1).occupant.nonEmpty) + assert(vehicle.MountedIn.nonEmpty) + assert(lodestar.CargoHolds(1).isOccupied) vehicle.Actor ! Vehicle.Deconstruct() - val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds) + val vehicle_msg = vehicleProbe.receiveN(6, 1 minute) //dismounting as cargo messages - assert( - vehicle_msg.head match { - case VehicleServiceMessage( - _, - VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(1) match { - case VehicleServiceMessage( - _, - VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(2) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) - ) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(3) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(4) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) - ) - ) => - true - case _ => false - } - ) - //dismounting as cargo messages - //TODO: does not actually kick out the cargo, but instigates the process - assert( - vehicle_msg(5) match { - case VehicleServiceMessage( - "test", - VehicleAction.KickPassenger(PlanetSideGUID(3), 4, false, PlanetSideGUID(1)) - ) => - true - case _ => false - } - ) + vehicle_msg.head match { + case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(3), 4, true, PlanetSideGUID(1))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedInTest-1: ${vehicle_msg.head}") + } + vehicle_msg(1) match { + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedInTest-2: ${vehicle_msg(1)}") + } + vehicle_msg(2) match { + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedInTest-3: ${vehicle_msg(2)}") + } + vehicle_msg(3) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedInTest-4: ${vehicle_msg(3)}") + } + vehicle_msg(4) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedInTest-5: ${vehicle_msg(4)}") + } + vehicle_msg(5) match { + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedInTest-6: ${vehicle_msg(5)}") + } assert(player1.VehicleSeated.isEmpty) assert(vehicle.Seats(1).occupant.isEmpty) + assert(vehicle.MountedIn.isEmpty) + assert(!lodestar.CargoHolds(1).isOccupied) } } } @@ -251,55 +217,37 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds) vehicle_msg(5) match { - case VehicleServiceMessage( - "test", - VehicleAction.KickPassenger(PlanetSideGUID(4), 4, false, PlanetSideGUID(2)) - ) => ; - case _ => assert(false) + case VehicleServiceMessage("test", VehicleAction.KickPassenger(PlanetSideGUID(4), 4, true, PlanetSideGUID(2))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-1: ${vehicle_msg(5)}") } assert(player2.VehicleSeated.isEmpty) assert(lodestar.Seats(0).occupant.isEmpty) //cargo dismounting messages vehicle_msg.head match { - case VehicleServiceMessage( - _, - VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _)) - ) => ; - case _ => assert(false) + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-2: ${vehicle_msg.head}") } vehicle_msg(1) match { - case VehicleServiceMessage( - _, - VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _)) - ) => ; - case _ => assert(false) + case VehicleServiceMessage(_, VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-3: ${vehicle_msg(1)}") } vehicle_msg(2) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) - ) - ) => ; - case _ => assert(false) + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-4: ${vehicle_msg(2)}") } vehicle_msg(3) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _)) - ) => ; - case _ => assert(false) + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-5: ${vehicle_msg(3)}") } vehicle_msg(4) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) - ) - ) => ; - case _ => assert(false) + case VehicleServiceMessage("test", VehicleAction.SendResponse(_, CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0))) => ; + case _ => + assert(false, s"VehicleControlPrepareForDeletionMountedCargoTest-6: ${vehicle_msg(4)}") } } }