diff --git a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala index 443e515a..fdfcd3f1 100644 --- a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala +++ b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala @@ -23,6 +23,8 @@ import net.psforever.packet._ import net.psforever.packet.control._ import net.psforever.packet.crypto.{ClientChallengeXchg, ClientFinished, ServerChallengeXchg, ServerFinished} import net.psforever.packet.game._ +import net.psforever.packet.crypto._ +import net.psforever.packet.game.{ChangeFireModeMessage, CharacterInfoMessage, KeepAliveMessage, PingMsg} import net.psforever.packet.PacketCoding.CryptoCoding import net.psforever.util.{Config, DiffieHellman, Md5Mac} @@ -382,7 +384,7 @@ class MiddlewareActor( PacketCoding.unmarshalPacket(msg, None, CryptoPacketOpcode.ClientFinished) match { case Successful(packet) => packet match { - case (ClientFinished(clientPubKey, _), Some(_)) => + case (ClientFinished(_, clientPubKey, _), Some(_)) => serverMACBuffer ++= msg.drop(3) val agreedKey = dh.agree(clientPubKey.toArray) val agreedMessage = ByteVector("master secret".getBytes) ++ clientChallenge ++ @@ -439,6 +441,8 @@ class MiddlewareActor( .receiveMessage[Command] { case Receive(msg) => PacketCoding.unmarshalPacket(msg, crypto) match { + case Successful((Ignore(data), _)) => + log.info(s"caught CryptoPacket Ignore with hex - $data") case Successful((packet, Some(sequence))) => activeSequenceFunc(packet, sequence) case Successful((packet, None)) => diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index fc982b79..c1f0f817 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -374,7 +374,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case out @ Some(obj) if obj.HasGUID => out - case None if id.nonEmpty && id.get != PlanetSideGUID(0) => + case None if !id.contains(PlanetSideGUID(0)) => //delete stale entity reference from client log.error(s"${player.Name} has an invalid reference to GUID ${id.get.guid} in zone ${continent.id}") sendResponse(ObjectDeleteMessage(id.get, 0)) @@ -4873,6 +4873,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.error( s"telepad@${object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}" ) + obj.Actor ! Deployable.Deconstruct() case None => ; } } @@ -5427,6 +5428,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con dismountWarning( s"DismountVehicleMsg: player ${player.Name}_guid not considered seated in a mountable entity" ) + sendResponse(DismountVehicleMsg(player_guid, bailType, wasKickedByDriver)) None }) match { case Some(_) if serverVehicleControlVelocity.nonEmpty => @@ -5710,6 +5712,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con progressBarUpdate.cancel() progressBarValue = None + case msg @ TradeMessage(_,_) => + log.info(s"${player.Name} wants to trade, for some reason - $msg") + case _ => log.warn(s"Unhandled GamePacket $pkt") } @@ -6362,39 +6367,42 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { case Nil => ; case x :: xs => - val (deleteFunc, modifyFunc): (Equipment => Future[Any], (AmmoBox, Int) => Unit) = obj match { - case veh: Vehicle => - (RemoveOldEquipmentFromInventory(veh), ModifyAmmunitionInVehicle(veh)) - case _ => - (RemoveOldEquipmentFromInventory(obj), ModifyAmmunition(obj)) + val modifyFunc: (AmmoBox, Int) => Unit = obj match { + case veh: Vehicle => ModifyAmmunitionInVehicle(veh) + case _ => ModifyAmmunition(obj) } - val (stowNewFunc, stowFunc): (Equipment => TaskResolver.GiveTask, Equipment => Future[Any]) = - (PutNewEquipmentInInventoryOrDrop(obj), PutEquipmentInInventoryOrDrop(obj)) + val stowNewFunc: Equipment => TaskResolver.GiveTask = PutNewEquipmentInInventoryOrDrop(obj) + val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj) xs.foreach(item => { - obj.Inventory -= x.start - deleteFunc(item.obj) + obj.Inventory -= item.start + sendResponse(ObjectDeleteMessage(item.obj.GUID, 0)) + continent.tasks ! GUIDTask.UnregisterObjectTask(item.obj)(continent.GUID) }) - //box will be the replacement ammo; give it the discovered magazine and load it into the weapon @ 0 + //box will be the replacement ammo; give it the discovered magazine and load it into the weapon val box = x.obj.asInstanceOf[AmmoBox] + //previousBox is the current magazine in tool; it will be removed from the weapon + val previousBox = tool.AmmoSlot.Box val originalBoxCapacity = box.Capacity - val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum } + val tailReloadValue: Int = if (xs.isEmpty) { + 0 + } else { + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum + } val sumReloadValue: Int = originalBoxCapacity + tailReloadValue - val previousBox = tool.AmmoSlot.Box //current magazine in tool - sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f)) - sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f)) + val ammoSlotIndex = tool.FireMode.AmmoSlotIndex + val box_guid = box.GUID + val tool_guid = tool.GUID obj.Inventory -= x.start //remove replacement ammo from inventory - val ammoSlotIndex = tool.FireMode.AmmoSlotIndex tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool - sendResponse(ObjectAttachMessage(tool.GUID, box.GUID, ammoSlotIndex)) + sendResponse(ObjectDetachMessage(tool_guid, previousBox.GUID, Vector3.Zero, 0f)) + sendResponse(ObjectDetachMessage(obj.GUID, box_guid, Vector3.Zero, 0f)) + sendResponse(ObjectAttachMessage(tool_guid, box_guid, ammoSlotIndex)) //announce swapped ammunition box in weapon val previous_box_guid = previousBox.GUID val boxDef = box.Definition - val box_guid = box.GUID - val tool_guid = tool.GUID sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) continent.AvatarEvents ! AvatarServiceMessage( continent.id, diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 46ac7d6c..71b7c66e 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -277,7 +277,7 @@ class Player(var avatar: Avatar) None } else { val slot = iter.next() - if (slot.Equipment.isDefined && slot.Equipment.get.GUID == guid) { + if (slot.Equipment match { case Some(o) => o.GUID == guid; case _ => false }) { Some(index) } else { findInHolsters(iter, guid, index + 1) diff --git a/src/main/scala/net/psforever/objects/ce/TelepadLike.scala b/src/main/scala/net/psforever/objects/ce/TelepadLike.scala index 9f1ad124..1f0ea250 100644 --- a/src/main/scala/net/psforever/objects/ce/TelepadLike.scala +++ b/src/main/scala/net/psforever/objects/ce/TelepadLike.scala @@ -167,9 +167,10 @@ class TelepadControl(obj: InternalTelepad) extends akka.actor.Actor { obj.Active = false val zone = obj.Zone zone.GUID(obj.Telepad) match { - case Some(oldTpad: TelepadDeployable) if !obj.Active && !setup.isCancelled => + case Some(oldTpad: TelepadDeployable) + if !obj.Active && !setup.isCancelled => oldTpad.Actor ! TelepadLike.SeverLink(obj) - case None => ; + case _ => ; } obj.Telepad = None zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SendResponse(ObjectDeleteMessage(obj.GUID, 0))) diff --git a/src/main/scala/net/psforever/packet/CryptoPacketOpcode.scala b/src/main/scala/net/psforever/packet/CryptoPacketOpcode.scala index 4a345f0b..25647f98 100644 --- a/src/main/scala/net/psforever/packet/CryptoPacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/CryptoPacketOpcode.scala @@ -8,8 +8,9 @@ object CryptoPacketOpcode extends Enumeration { type Type = Value val Ignore, ClientChallengeXchg, ServerChallengeXchg, ClientFinished, ServerFinished = Value - def getPacketDecoder(opcode: CryptoPacketOpcode.Type): (BitVector) => Attempt[DecodeResult[PlanetSideCryptoPacket]] = + def getPacketDecoder(opcode: CryptoPacketOpcode.Type): BitVector => Attempt[DecodeResult[PlanetSideCryptoPacket]] = opcode match { + case Ignore => crypto.Ignore.decode case ClientChallengeXchg => crypto.ClientChallengeXchg.decode case ServerChallengeXchg => crypto.ServerChallengeXchg.decode case ServerFinished => crypto.ServerFinished.decode @@ -17,7 +18,7 @@ object CryptoPacketOpcode extends Enumeration { case default => (a: BitVector) => Attempt.failure( - Err(s"Could not find a marshaller for crypto packet ${opcode}") + Err(s"Could not find a marshaller for crypto packet $opcode") .pushContext("get_marshaller") ) } diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 97ec7a42..64fa94f2 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -446,7 +446,7 @@ object GamePacketOpcode extends Enumeration { case 0x77 => game.SquadState.decode // 0x78 case 0x78 => game.OxygenStateMessage.decode - case 0x79 => noDecoder(TradeMessage) + case 0x79 => game.TradeMessage.decode case 0x7a => noDecoder(UnknownMessage122) case 0x7b => game.DamageFeedbackMessage.decode case 0x7c => game.DismountBuildingMsg.decode diff --git a/src/main/scala/net/psforever/packet/crypto/ClientFinished.scala b/src/main/scala/net/psforever/packet/crypto/ClientFinished.scala index 49956a3e..06318193 100644 --- a/src/main/scala/net/psforever/packet/crypto/ClientFinished.scala +++ b/src/main/scala/net/psforever/packet/crypto/ClientFinished.scala @@ -2,22 +2,43 @@ package net.psforever.packet.crypto import net.psforever.packet.{CryptoPacketOpcode, Marshallable, PlanetSideCryptoPacket} +import scodec.Attempt.Successful import scodec.Codec import scodec.bits.{ByteVector, _} import scodec.codecs._ +import shapeless.{::, HNil} -final case class ClientFinished(pubKey: ByteVector, challengeResult: ByteVector) extends PlanetSideCryptoPacket { +final case class ClientFinished( + obj_type: Int, + pubKey: ByteVector, + challengeResult: ByteVector + ) extends PlanetSideCryptoPacket { type Packet = ClientFinished def opcode = CryptoPacketOpcode.ClientFinished def encode = ClientFinished.encode(this) } object ClientFinished extends Marshallable[ClientFinished] { + def apply(pubKey: ByteVector, challengeResult: ByteVector): ClientFinished = + ClientFinished(16, pubKey, challengeResult) + implicit val codec: Codec[ClientFinished] = ( - ("obj_type?" | constant(hex"10".bits)) :: + ("obj_type?" | uint8) :: ("pub_key_len" | constant(hex"1000")) :: ("pub_key" | bytes(16)) :: ("unknown" | constant(hex"0114".bits)) :: ("challenge_result" | bytes(0xc)) - ).as[ClientFinished] + ).exmap[ClientFinished]( + { + case 16 :: () :: c :: () :: e :: HNil => + Successful(ClientFinished(16, c, e)) + case a :: () :: c :: () :: e :: HNil => + org.log4s.getLogger("ClientFinished").warn(s"obj_type?: expected 16 but got $a; attempting to bypass") + Successful(ClientFinished(a, c, e)) + }, + { + case ClientFinished(a, c, e) => + Successful(a :: () :: c :: () :: e :: HNil) + } + ) } diff --git a/src/main/scala/net/psforever/packet/crypto/Ignore.scala b/src/main/scala/net/psforever/packet/crypto/Ignore.scala new file mode 100644 index 00000000..9967861d --- /dev/null +++ b/src/main/scala/net/psforever/packet/crypto/Ignore.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.crypto + +import net.psforever.packet.{CryptoPacketOpcode, Marshallable, PlanetSideCryptoPacket} +import scodec.Codec +import scodec.bits.ByteVector +import scodec.codecs._ + +final case class Ignore(data: ByteVector) extends PlanetSideCryptoPacket { + type Packet = Ignore + def opcode = CryptoPacketOpcode.Ignore + def encode = Ignore.encode(this) +} + +object Ignore extends Marshallable[Ignore] { + implicit val codec: Codec[Ignore] = ("data" | bytes).as[Ignore] +} + diff --git a/src/main/scala/net/psforever/packet/game/TradeMessage.scala b/src/main/scala/net/psforever/packet/game/TradeMessage.scala new file mode 100644 index 00000000..c3f83610 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/TradeMessage.scala @@ -0,0 +1,111 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.PlanetSideGUID +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +sealed trait Trade { + def value: Int +} + +final case class NoTrade(value: Int) extends Trade { + assert(value == 1 || value == 2 || value == 3, s"NoTrade has wrong code value - $value not in [a-f]") +} + +final case class TradeOne(value: Int, unk1: PlanetSideGUID, unk2: PlanetSideGUID, unk3: PlanetSideGUID) extends Trade { + assert(value == 1 || value == 2 || value == 3, s"TradeOne has wrong code value - $value not in [1,2,3]") +} + +final case class TradeTwo(value: Int, unk1: PlanetSideGUID, unk2: PlanetSideGUID) extends Trade { + assert(value == 4 || value == 5 || value == 7, s"TradeTwo has wrong code value - $value not in [4,5,7]") +} + +final case class TradeThree(value: Int, unk: PlanetSideGUID) extends Trade { + assert(value == 6 || value == 8, s"TradeThree has wrong code value - $value not in [6,8]") +} + +final case class TradeFour(value: Int, unk: Int) extends Trade { + assert(value == 9, s"TradeFour has wrong code value - $value not in [9]") +} + +final case class TradeMessage(unk: Int, trade: Trade) + extends PlanetSideGamePacket { + type Packet = TradeMessage + def opcode = GamePacketOpcode.TradeMessage + def encode = TradeMessage.encode(this) +} + +object TradeMessage extends Marshallable[TradeMessage] { + private def tradeOneCodec(value: Int): Codec[TradeOne] = ( + ("u1" | PlanetSideGUID.codec) :: + ("u2" | PlanetSideGUID.codec) :: + ("u3" | PlanetSideGUID.codec) + ).xmap[TradeOne]( + { + case a :: b :: c :: HNil => TradeOne(value, a, b, c) + }, + { + case TradeOne(_, a, b, c) => a :: b :: c :: HNil + } + ) + + private def tradeTwoCodec(value: Int): Codec[TradeTwo] = ( + ("u1" | PlanetSideGUID.codec) :: + ("u2" | PlanetSideGUID.codec) + ).xmap[TradeTwo]( + { + case a :: b :: HNil => TradeTwo(value, a, b) + }, + { + case TradeTwo(_, a, b) => a :: b :: HNil + } + ) + + private def tradeThreeCodec(value: Int): Codec[TradeThree] = ("u1" | PlanetSideGUID.codec).xmap[TradeThree]( + a => TradeThree(value, a), + { + case TradeThree(_, a) => a + } + ) + + private def tradeFourCodec(value: Int): Codec[TradeFour] = ("u1" | uint4).xmap[TradeFour]( + a => TradeFour(value, a), + { + case TradeFour(_, a) => a + } + ) + + private def noTradeCodec(value: Int): Codec[NoTrade] = conditional(included = false, ignore(size = 1)).xmap[NoTrade]( + _ => NoTrade(value), + { + case NoTrade(_) => None + } + ) + + private def selectTradeCodec(code: Int): Codec[Trade] = { + (code match { + case 1 | 2 | 3 => tradeOneCodec(code) + case 4 | 5 | 7 => tradeTwoCodec(code) + case 6 | 8 => tradeThreeCodec(code) + case 9 => tradeFourCodec(code) + case _ => noTradeCodec(code) + }).asInstanceOf[Codec[Trade]] + } + + implicit val codec: Codec[TradeMessage] = ( + ("unk" | uint8) :: + (uint4 >>:~ { code => + ("trade" | selectTradeCodec(code)).hlist + }) + ).xmap[TradeMessage]( + { + case unk :: _ :: trade :: HNil => TradeMessage(unk, trade) + }, + { + case TradeMessage(unk, trade) => unk :: trade.value :: trade :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index b4db3f5e..024452ed 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -2067,7 +2067,12 @@ class SquadService extends Actor { case (Some((fromMember, fromPosition)), (toMember, _)) if fromPosition != 0 => val name = fromMember.Name SwapMemberPosition(toMember, fromMember) - Publish(squadFeatures(guid).ToChannel, SquadResponse.AssignMember(squad, fromPosition, position)) + squadFeatures.get(guid) match { + case Some(features) => + Publish(features.ToChannel, SquadResponse.AssignMember(squad, fromPosition, position)) + case None => + //we might be in trouble; we might not ... + } UpdateSquadDetail( squad.GUID, SquadDetail().Members(