diff --git a/.codecov.yml b/.codecov.yml index 351d39df..ca48a57c 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,6 +1,13 @@ # Too spammy for us comment: off +coverage: + status: + project: + default: + target: auto + threshold: 0.25% + ignore: - "src/main/scala/net/psforever/objects/ObjectType.scala" - "src/main/scala/net/psforever/objects/avatar/Avatars.scala" @@ -14,6 +21,7 @@ ignore: - "src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala" - "src/main/scala/net/psforever/objects/serverobject/pad/AutoDriveControls.scala" - "src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala" + - "src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala" - "src/main/scala/net/psforever/objects/serverobject/turret/TurretUpgrade.scala" - "src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala" - "src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala" @@ -63,6 +71,8 @@ ignore: - "src/main/scala/net/psforever/services/avatar/AvatarResponse.scala" - "src/main/scala/net/psforever/services/galaxy/GalaxyAction.scala" - "src/main/scala/net/psforever/services/galaxy/GalaxyResponse.scala" + - "src/main/scala/net/psforever/services/hart/HartEvent.scala" + - "src/main/scala/net/psforever/services/hart/HartTimerActions.scala" - "src/main/scala/net/psforever/services/local/LocalAction.scala" - "src/main/scala/net/psforever/services/local/LocalResponse.scala" - "src/main/scala/net/psforever/services/vehicle/VehicleAction.scala" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0506dc74..b6cf88a9 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -21,7 +21,7 @@ jobs: - name: Build docs run: sbt docs/unidoc - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@3.7.1 + uses: JamesIves/github-pages-deploy-action@4.1.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages @@ -38,7 +38,7 @@ jobs: run: | echo "REPOSITORY=$(echo $GITHUB_REPOSITORY | tr '[A-Z]' '[a-z]')" >> $GITHUB_ENV - name: Build and push Docker image - uses: docker/build-push-action@v2.2.2 + uses: docker/build-push-action@v2.3.0 with: username: ${{ github.actor }} password: ${{ github.token }} diff --git a/README.md b/README.md index b77f67d7..86c7c94d 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The user should be created and made owner of the database. ```sql CREATE USER psforever; ALTER USER psforever WITH PASSWORD 'psforever'; -ALTER TABLE psforever OWNER TO psforever; +ALTER DATABASE psforever OWNER TO psforever; ``` **NOTE:** applying default privileges _after_ importing the schema will not apply them to existing objects. To fix this, *you must drop all objects and try again or apply permissions manually using the Query Tool / `psql`. diff --git a/build.sbt b/build.sbt index 12601dab..a088d5e5 100644 --- a/build.sbt +++ b/build.sbt @@ -40,45 +40,47 @@ lazy val psforeverSettings = Seq( classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat, resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", libraryDependencies ++= Seq( - "com.typesafe.akka" %% "akka-actor" % "2.6.11", - "com.typesafe.akka" %% "akka-slf4j" % "2.6.11", - "com.typesafe.akka" %% "akka-protobuf-v3" % "2.6.11", - "com.typesafe.akka" %% "akka-stream" % "2.6.11", - "com.typesafe.akka" %% "akka-testkit" % "2.6.11" % "test", - "com.typesafe.akka" %% "akka-actor-typed" % "2.6.11", - "com.typesafe.akka" %% "akka-cluster-typed" % "2.6.11", - "com.typesafe.akka" %% "akka-coordination" % "2.6.11", - "com.typesafe.akka" %% "akka-cluster-tools" % "2.6.11", - "com.typesafe.akka" %% "akka-slf4j" % "2.6.11", - "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2", + "com.typesafe.akka" %% "akka-actor" % "2.6.13", + "com.typesafe.akka" %% "akka-slf4j" % "2.6.13", + "com.typesafe.akka" %% "akka-protobuf-v3" % "2.6.13", + "com.typesafe.akka" %% "akka-stream" % "2.6.13", + "com.typesafe.akka" %% "akka-testkit" % "2.6.13" % "test", + "com.typesafe.akka" %% "akka-actor-typed" % "2.6.13", + "com.typesafe.akka" %% "akka-cluster-typed" % "2.6.13", + "com.typesafe.akka" %% "akka-coordination" % "2.6.13", + "com.typesafe.akka" %% "akka-cluster-tools" % "2.6.13", + "com.typesafe.akka" %% "akka-slf4j" % "2.6.13", + "com.typesafe.akka" %% "akka-http" % "10.2.4", + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.3", "org.specs2" %% "specs2-core" % "4.10.6" % "test", - "org.scalatest" %% "scalatest" % "3.2.3" % "test", + "org.scalatest" %% "scalatest" % "3.2.6" % "test", "org.scodec" %% "scodec-core" % "1.11.7", "ch.qos.logback" % "logback-classic" % "1.2.3", "org.log4s" %% "log4s" % "1.9.0", - "org.fusesource.jansi" % "jansi" % "2.1.1", + "org.fusesource.jansi" % "jansi" % "2.3.2", "org.scoverage" %% "scalac-scoverage-plugin" % "1.4.2", "com.github.nscala-time" %% "nscala-time" % "2.26.0", "com.github.t3hnar" %% "scala-bcrypt" % "4.3.0", "org.scala-graph" %% "graph-core" % "1.13.2", - "io.kamon" %% "kamon-bundle" % "2.1.10", - "io.kamon" %% "kamon-apm-reporter" % "2.1.10", - "org.json4s" %% "json4s-native" % "3.6.10", - "io.getquill" %% "quill-jasync-postgres" % "3.6.0", - "org.flywaydb" % "flyway-core" % "7.5.0", - "org.postgresql" % "postgresql" % "42.2.18", + "io.kamon" %% "kamon-bundle" % "2.1.13", + "io.kamon" %% "kamon-apm-reporter" % "2.1.13", + "org.json4s" %% "json4s-native" % "3.6.11", + "io.getquill" %% "quill-jasync-postgres" % "3.7.0", + "org.flywaydb" % "flyway-core" % "7.7.1", + "org.postgresql" % "postgresql" % "42.2.19", "com.typesafe" % "config" % "1.4.1", - "com.github.pureconfig" %% "pureconfig" % "0.14.0", + "com.github.pureconfig" %% "pureconfig" % "0.14.1", "com.beachape" %% "enumeratum" % "1.6.1", - "joda-time" % "joda-time" % "2.10.9", + "joda-time" % "joda-time" % "2.10.10", "commons-io" % "commons-io" % "2.8.0", - "com.github.scopt" %% "scopt" % "4.0.0", - "io.sentry" % "sentry-logback" % "3.2.1", + "com.github.scopt" %% "scopt" % "4.0.1", + "io.sentry" % "sentry-logback" % "4.3.0", "io.circe" %% "circe-core" % "0.13.0", "io.circe" %% "circe-generic" % "0.13.0", "io.circe" %% "circe-parser" % "0.13.0", - "org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.0", - "org.bouncycastle" % "bcprov-jdk15on" % "1.68" + "org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.1", + "org.bouncycastle" % "bcprov-jdk15on" % "1.68", + "org.codehaus.janino" % "janino" % "3.1.3" ), // TODO(chord): remove exclusion when SessionActor is refactored: https://github.com/psforever/PSF-LoginServer/issues/279 coverageExcludedPackages := "net\\.psforever\\.actors\\.session\\.SessionActor.*" diff --git a/config/logback.xml b/config/logback.xml index 29871318..60bcdffd 100644 --- a/config/logback.xml +++ b/config/logback.xml @@ -10,36 +10,116 @@ - - logs/pslogin-debug_${bySecond}.log + + logs/psforever-general_${bySecond}.log + + logs/psforever-general_%d{yyyy-MM-dd}.gz + 60 + 10GB + - %date{ISO8601} %5level "%X" %logger{35} - %msg%n + %date{ISO8601} %5level %logger{35} - %msg%n + + + + encrypted + + Unexpected packet type EncryptedPacket + + encrypted.matches(formattedMessage) + + DENY + NEUTRAL + + + + com.github.jasync.sql.db.postgresql.codec + + + + io.sentry.connection + + + + DamageResolution + - DEBUG + INFO + + + 5000 - - logs/pslogin-trace_${bySecond}.log + + logs/psforever-debug_${bySecond}.log + + logs/psforever-debug_%d{yyyy-MM-dd}.gz + 60 + 10GB + %date{ISO8601} [%thread] %5level "%X" %logger{35} - %msg%n - - OFF - + + + com.github.jasync.sql.db.postgresql.encoders + + + com.github.jasync.sql.db.postgresql.codec + + + + io.getquill.context.jasync + + + DEBUG + ACCEPT + DENY + + WARN + + + + encrypted + + Unexpected packet type EncryptedPacket + + encrypted.matches(formattedMessage) + + DENY + NEUTRAL + - + diff --git a/project/plugins.sbt b/project/plugins.sbt index bfee58e2..f118e14a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ logLevel := Level.Warn addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.13") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") -addSbtPlugin("io.kamon" % "sbt-kanela-runner" % "2.0.7") +addSbtPlugin("io.kamon" % "sbt-kanela-runner" % "2.0.10") addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.25") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.26") diff --git a/renovate.json b/renovate.json index e82850c7..5f4a65e9 100644 --- a/renovate.json +++ b/renovate.json @@ -14,6 +14,10 @@ { "groupName": "circe", "packagePatterns": "^io.circe" + }, + { + "groupName": "kamon", + "packagePatterns": "^io.kamon" } ] } diff --git a/server/src/main/java/net/psforever/filters/ApplyCooldownToDuplicateLoggingFilter.java b/server/src/main/java/net/psforever/filters/ApplyCooldownToDuplicateLoggingFilter.java new file mode 100644 index 00000000..38a77d02 --- /dev/null +++ b/server/src/main/java/net/psforever/filters/ApplyCooldownToDuplicateLoggingFilter.java @@ -0,0 +1,77 @@ +// Copyright (c) 2021 PSForever +package net.psforever.filters; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Disrupts a variety of logging messages that would otherwise repeat within a certain frame of time. + * Until there is a significant break in time between the logging of the duplicated messages, + * those messages are denied logging. + * Only exact matches via hash are denied. + * Be aware of the pitfalls of default `String` hash code. + */ +public class ApplyCooldownToDuplicateLoggingFilter extends Filter { + private long cooldown; + private ConcurrentHashMap messageMap; + private long cleaning = 900000L; //default: 15min + private ScheduledExecutorService housecleaning; + + @Override + public FilterReply decide(ILoggingEvent event) { + String msg = event.getMessage(); + long currTime = System.currentTimeMillis(); + Long previousTime = messageMap.put(msg, currTime); + if (previousTime != null && previousTime + cooldown > currTime) { + return FilterReply.DENY; + } else { + return FilterReply.NEUTRAL; + } + } + + public void setCooldown(Long duration) { + this.cooldown = duration; + } + + public void setCleaning(Long duration) { + this.cleaning = duration; + } + + @Override + public void start() { + if (this.cooldown != 0L) { + messageMap = new ConcurrentHashMap<>(1000); + housecleaning = Executors.newScheduledThreadPool(1); + Runnable task = () -> { + //being "concurrent" should be enough + //the worst that can happen is two of the same message back-to-back in the log once in a while + if (!messageMap.isEmpty()) { + long currTime = System.currentTimeMillis(); + Iterator oldLogMessages = messageMap.entrySet().stream() + .filter( entry -> entry.getValue() + cooldown < currTime ) + .map( Map.Entry::getKey ) + .iterator(); + oldLogMessages.forEachRemaining(key -> messageMap.remove(key)); + } + }; + housecleaning.scheduleWithFixedDelay(task, cleaning, cleaning, TimeUnit.MILLISECONDS); + super.start(); + } + } + + @Override + public void stop() { + housecleaning.shutdown(); + messageMap.clear(); + messageMap = null; + super.stop(); + } +} diff --git a/server/src/main/java/net/psforever/filters/LoggerPrefixFilter.java b/server/src/main/java/net/psforever/filters/LoggerPrefixFilter.java new file mode 100644 index 00000000..851b3282 --- /dev/null +++ b/server/src/main/java/net/psforever/filters/LoggerPrefixFilter.java @@ -0,0 +1,38 @@ +// Copyright (c) 2021 PSForever +package net.psforever.filters; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; + +/** + * Disrupts a variety of logging messages that originate from specific loggers. + * A comparison of the prefix text of the logger handling the event is performed, + * with a positive match denying that event being appended. + * The full prefix must be provided, as the filter uses the fully authenticated name + * and the logger occasionally displays an abbreviated form for longer names, + * e.g., "i.g.context.jasync ..." instead of "io.getquill.context.jasync ...". + */ +public class LoggerPrefixFilter extends Filter { + private String prefix; + + @Override + public FilterReply decide(ILoggingEvent event) { + if (isStarted() && event.getLoggerName().startsWith(prefix)) { + return FilterReply.DENY; + } else { + return FilterReply.NEUTRAL; + } + } + + public void setPrefix(String name) { + this.prefix = name; + } + + @Override + public void start() { + if (this.prefix != null) { + super.start(); + } + } +} diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index 2b6529ea..a44d9237 100644 --- a/server/src/main/resources/overrides/game_objects0.adb.lst +++ b/server/src/main/resources/overrides/game_objects0.adb.lst @@ -1,7 +1,7 @@ -add_property ace allowed false +add_property ace allowed true add_property ace equiptime 500 add_property ace holstertime 500 -add_property ace_deployable allowed false +add_property ace_deployable allowed true add_property ace_deployable equiptime 500 add_property ace_deployable holstertime 500 add_property advanced_ace equiptime 750 diff --git a/server/src/main/scala/net/psforever/server/Server.scala b/server/src/main/scala/net/psforever/server/Server.scala index 5aeae7c6..9612fc2d 100644 --- a/server/src/main/scala/net/psforever/server/Server.scala +++ b/server/src/main/scala/net/psforever/server/Server.scala @@ -36,6 +36,7 @@ import org.slf4j import scopt.OParser import akka.actor.typed.scaladsl.adapter._ import net.psforever.packet.PlanetSidePacket +import net.psforever.services.hart.HartService object Server { private val logger = org.log4s.getLogger @@ -129,6 +130,7 @@ object Server { serviceManager ! ServiceManager.Register(classic.Props[SquadService](), "squad") serviceManager ! ServiceManager.Register(classic.Props[AccountPersistenceService](), "accountPersistence") serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager") + serviceManager ! ServiceManager.Register(classic.Props[HartService](), "hart") system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.login.port), login), "login-socket") system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket") diff --git a/server/src/test/scala/PacketCodingActorTest.scala b/server/src/test/scala/PacketCodingActorTest.scala index 724b1463..7450451a 100644 --- a/server/src/test/scala/PacketCodingActorTest.scala +++ b/server/src/test/scala/PacketCodingActorTest.scala @@ -485,7 +485,7 @@ class PacketCodingActorITest extends ActorTest { BasicCharacterData( "IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 41, CharacterVoice.Voice1 ), @@ -600,7 +600,7 @@ class PacketCodingActorKTest extends ActorTest { BasicCharacterData( "IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 41, CharacterVoice.Voice1 ), diff --git a/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala b/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala index 7da3afee..cc3ea960 100644 --- a/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala +++ b/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala @@ -48,7 +48,7 @@ class AutoRepairFacilityIntegrationTest extends FreedContextActorTest { val building = Building.Structure(StructureType.Facility)(name = "integ-fac-test-building", guid = 6, map_id = 0, zone, context) building.Invalidate() - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val weapon = new Tool(GlobalDefinitions.suppressor) val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition) @@ -164,7 +164,7 @@ class AutoRepairFacilityIntegrationAntGiveNtuTest extends FreedContextActorTest expectNoMessage(1000 milliseconds) var buildingMap = new TrieMap[Int, Building]() val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val ant = Vehicle(GlobalDefinitions.ant) val terminal = new Terminal(AutoRepairIntegrationTest.slow_terminal_definition) val silo = new ResourceSilo() @@ -203,7 +203,7 @@ class AutoRepairFacilityIntegrationAntGiveNtuTest extends FreedContextActorTest ant.NtuCapacitor = maxNtuCap ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Zone = zone - ant.Seats(0).Occupant = player + ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed building.Amenities = terminal building.Amenities = silo @@ -255,7 +255,7 @@ class AutoRepairFacilityIntegrationTerminalDestroyedTerminalAntTest extends Free expectNoMessage(1000 milliseconds) var buildingMap = new TrieMap[Int, Building]() val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val weapon = new Tool(GlobalDefinitions.suppressor) val ant = Vehicle(GlobalDefinitions.ant) val terminal = new Terminal(AutoRepairIntegrationTest.slow_terminal_definition) @@ -297,7 +297,7 @@ class AutoRepairFacilityIntegrationTerminalDestroyedTerminalAntTest extends Free ant.NtuCapacitor = maxNtuCap ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Zone = zone - ant.Seats(0).Occupant = player + ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed building.Amenities = terminal building.Amenities = silo @@ -357,7 +357,7 @@ class AutoRepairFacilityIntegrationTerminalIncompleteRepairTest extends FreedCon expectNoMessage(1000 milliseconds) var buildingMap = new TrieMap[Int, Building]() val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val weapon = new Tool(GlobalDefinitions.suppressor) val ant = Vehicle(GlobalDefinitions.ant) val terminal = new Terminal(AutoRepairIntegrationTest.slow_terminal_definition) @@ -399,7 +399,7 @@ class AutoRepairFacilityIntegrationTerminalIncompleteRepairTest extends FreedCon ant.NtuCapacitor = maxNtuCap ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") ant.Zone = zone - ant.Seats(0).Occupant = player + ant.Seats(0).mount(player) ant.DeploymentState = DriveState.Deployed building.Amenities = terminal building.Amenities = silo @@ -485,7 +485,7 @@ class AutoRepairTowerIntegrationTest extends FreedContextActorTest { val building = Building.Structure(StructureType.Tower)(name = "integ-twr-test-building", guid = 6, map_id = 0, zone, context) building.Invalidate() - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val weapon = new Tool(GlobalDefinitions.suppressor) val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition) diff --git a/server/src/test/scala/actor/objects/AutoRepairTest.scala b/server/src/test/scala/actor/objects/AutoRepairTest.scala index 3e9b3246..ef61ecbf 100644 --- a/server/src/test/scala/actor/objects/AutoRepairTest.scala +++ b/server/src/test/scala/actor/objects/AutoRepairTest.scala @@ -20,13 +20,13 @@ import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects.{GlobalDefinitions, Player, Tool} import net.psforever.services.ServiceManager -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, Vector3} import scala.concurrent.duration._ class AutoRepairRequestNtuTest extends FreedContextActorTest { ServiceManager.boot - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val weapon = new Tool(GlobalDefinitions.suppressor) val terminal = new Terminal(AutoRepairTest.terminal_definition) @@ -87,7 +87,7 @@ class AutoRepairRequestNtuTest extends FreedContextActorTest { class AutoRepairRequestNtuRepeatTest extends FreedContextActorTest { ServiceManager.boot - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val weapon = new Tool(GlobalDefinitions.suppressor) val terminal = new Terminal(AutoRepairTest.terminal_definition) @@ -151,7 +151,7 @@ class AutoRepairRequestNtuRepeatTest extends FreedContextActorTest { class AutoRepairNoRequestNtuTest extends FreedContextActorTest { ServiceManager.boot - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val weapon = new Tool(GlobalDefinitions.suppressor) val terminal = new Terminal(AutoRepairTest.terminal_definition) @@ -209,7 +209,7 @@ class AutoRepairNoRequestNtuTest extends FreedContextActorTest { class AutoRepairRestoreRequestNtuTest extends FreedContextActorTest { ServiceManager.boot - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val weapon = new Tool(GlobalDefinitions.suppressor) val terminal = new Terminal(AutoRepairTest.terminal_definition) @@ -274,7 +274,7 @@ class AutoRepairRestoreRequestNtuTest extends FreedContextActorTest { class AutoRepairRepairWithNtuTest extends FreedContextActorTest { ServiceManager.boot - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val weapon = new Tool(GlobalDefinitions.suppressor) val terminal = new Terminal(AutoRepairTest.terminal_definition) @@ -336,7 +336,7 @@ class AutoRepairRepairWithNtuTest extends FreedContextActorTest { class AutoRepairRepairWithNtuUntilDoneTest extends FreedContextActorTest { ServiceManager.boot - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val weapon = new Tool(GlobalDefinitions.suppressor) val terminal = new Terminal(AutoRepairTest.terminal_definition) diff --git a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala index 43fafd4f..81b3f0a2 100644 --- a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala +++ b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala @@ -39,7 +39,7 @@ class VehicleSpawnControl2Test extends ActorTest { probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.LoadVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.AttachToRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) - vehicle.Seats(0).Occupant = player + vehicle.Seats(0).mount(player) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.PlayerSeatedInVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DetachFromRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ServerVehicleOverrideStart]) @@ -58,7 +58,7 @@ class VehicleSpawnControl3Test extends ActorTest { val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) //we can recycle the vehicle and the player for each order val probe = new TestProbe(system, "zone-events") - val player2 = Player(Avatar(0, "test2", player.Faction, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player2 = Player(Avatar(0, "test2", player.Faction, CharacterSex.Male, 0, CharacterVoice.Mute)) player2.GUID = PlanetSideGUID(11) player2.Continent = zone.id player2.Spawn() @@ -75,7 +75,7 @@ class VehicleSpawnControl3Test extends ActorTest { probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.LoadVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.AttachToRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) - vehicle.Seats(0).Occupant = player + vehicle.Seats(0).mount(player) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.PlayerSeatedInVehicle]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.DetachFromRails]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ServerVehicleOverrideStart]) @@ -92,7 +92,7 @@ class VehicleSpawnControl3Test extends ActorTest { //if we move the vehicle away from the pad, we should receive a second ConcealPlayer message //that means that the first order has cleared and the spawn pad is now working on the second order successfully player.VehicleSeated = None //since shared between orders, as necessary - vehicle.Seats(0).Occupant = None + vehicle.Seats(0).unmount(player) vehicle.Position = Vector3(12, 0, 0) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ResetSpawnPad]) probe.expectMsgClass(3 seconds, classOf[VehicleSpawnPad.ConcealPlayer]) @@ -216,7 +216,7 @@ object VehicleSpawnPadControlTest { import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.vehicles.VehicleControl import net.psforever.objects.Tool - import net.psforever.types.CharacterGender + import net.psforever.types.CharacterSex val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) val weapon = vehicle.WeaponControlledFromSeat(1).get.asInstanceOf[Tool] @@ -245,7 +245,7 @@ object VehicleSpawnPadControlTest { pad.Owner.Faction = faction pad.Zone = zone guid.register(pad, "test-pool") - val player = Player(Avatar(0, "test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute)) guid.register(player, "test-pool") player.Zone = zone player.Spawn() diff --git a/server/src/test/scala/actor/service/AvatarServiceTest.scala b/server/src/test/scala/actor/service/AvatarServiceTest.scala index 0c05c783..ec324027 100644 --- a/server/src/test/scala/actor/service/AvatarServiceTest.scala +++ b/server/src/test/scala/actor/service/AvatarServiceTest.scala @@ -181,7 +181,7 @@ class DroptItemTest extends ActorTest { } class LoadPlayerTest extends ActorTest { - val obj = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + val obj = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.VS, CharacterSex.Female, 1, CharacterVoice.Voice1)) obj.GUID = PlanetSideGUID(10) obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(11) val c1data = obj.Definition.Packet.DetailedConstructorData(obj).get @@ -335,7 +335,7 @@ class PlayerStateTest extends ActorTest { } class PickupItemTest extends ActorTest { - val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterSex.Female, 1, CharacterVoice.Voice1)) val tool = Tool(GlobalDefinitions.beamer) tool.GUID = PlanetSideGUID(40) @@ -512,7 +512,7 @@ class AvatarReleaseTest extends FreedContextActorTest { GUID(guid) } zone.init(context) - val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterSex.Female, 1, CharacterVoice.Voice1)) guid.register(obj) guid.register(obj.Slot(5).Equipment.get) obj.Zone = zone @@ -563,7 +563,7 @@ class AvatarReleaseEarly1Test extends FreedContextActorTest { GUID(guid) } zone.init(context) - val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterSex.Female, 1, CharacterVoice.Voice1)) guid.register(obj) guid.register(obj.Slot(5).Equipment.get) obj.Zone = zone @@ -615,13 +615,13 @@ class AvatarReleaseEarly2Test extends FreedContextActorTest { GUID(guid) } zone.init(context) - val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterSex.Female, 1, CharacterVoice.Voice1)) guid.register(obj) guid.register(obj.Slot(5).Equipment.get) obj.Zone = zone obj.Release val objAlt = Player( - Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 1, CharacterVoice.Voice1) + Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 1, CharacterVoice.Voice1) ) //necessary clutter objAlt.GUID = PlanetSideGUID(3) objAlt.Slot(5).Equipment.get.GUID = PlanetSideGUID(4) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index ae205213..0bc62ab1 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -82,6 +82,15 @@ game { # Modify the amount of NTU drain per autorepair tick for facility amenities amenity-autorepair-drain-rate = 0.5 + # HART system, shuttles and facilities + hart { + # How long the shuttle is not boarding passengers (going through the motions) + in-flight-duration = 225000 + + # How long the shuttle allows passengers to board + boarding-duration = 60000 + } + new-avatar { # Starting battle rank br = 1 diff --git a/src/main/scala/net/psforever/actors/net/LoginActor.scala b/src/main/scala/net/psforever/actors/net/LoginActor.scala index 6632b3f9..dc6cd398 100644 --- a/src/main/scala/net/psforever/actors/net/LoginActor.scala +++ b/src/main/scala/net/psforever/actors/net/LoginActor.scala @@ -100,10 +100,9 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" if (token.isDefined) - log.info(s"New login UN:$username Token:${token.get}. $clientVersion") + log.trace(s"New login UN:$username Token:${token.get}. $clientVersion") else { -// log.info(s"New login UN:$username PW:$password. $clientVersion") - log.info(s"New login UN:$username. $clientVersion") + log.trace(s"New login UN:$username. $clientVersion") } accountLogin(username, password.get) @@ -115,7 +114,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne middlewareActor ! MiddlewareActor.Close() case _ => - log.debug(s"Unhandled GamePacket $pkt") + log.warn(s"Unhandled GamePacket $pkt") } def accountLogin(username: String, password: String): Unit = { @@ -197,7 +196,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne } def loginPwdFailureResponse(username: String, newToken: String) = { - log.info(s"Failed login to account $username") + log.warn(s"Failed login to account $username") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( newToken, @@ -212,7 +211,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne } def loginFailureResponse(username: String, newToken: String) = { - log.info("DB problem") + log.warn("DB problem") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( newToken, @@ -227,7 +226,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne } def loginAccountFailureResponse(username: String, newToken: String) = { - log.info(s"Account $username inactive") + log.warn(s"Account $username inactive") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( newToken, diff --git a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala index bd56e9f8..138f650f 100644 --- a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala +++ b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala @@ -119,10 +119,7 @@ object MiddlewareActor { packet.isInstanceOf[CharacterInfoMessage] } - /** - * `KeepAliveMessage` packets are bundled by themselves. - * They're special. - */ + /** `KeepAliveMessage` packets are bundled by themselves. They're special. */ def keepAliveMessageGuard(packet: PlanetSidePacket): Boolean = { packet.isInstanceOf[KeepAliveMessage] } @@ -295,7 +292,7 @@ class MiddlewareActor( Behaviors.same case _: ChangeFireModeMessage => - log.trace(s"What is this packet that just arrived? ${msg.toString}") + log.trace(s"What is this packet that just arrived? $msg") //ignore Behaviors.same @@ -420,7 +417,7 @@ class MiddlewareActor( case Successful((packet, None)) => in(packet) case Failure(e) => - log.error(s"could not decode packet: $e") + log.error(s"Could not decode $connectionId's packet: $e") } Behaviors.same @@ -530,7 +527,7 @@ class MiddlewareActor( def in(packet: Attempt[PlanetSidePacket]): Unit = { packet match { case Successful(_packet) => in(_packet) - case Failure(cause) => log.error(cause.message) + case Failure(cause) => log.error(s"Could not decode packet: ${cause.message}") } } @@ -543,7 +540,7 @@ class MiddlewareActor( case _ => PacketCoding.encodePacket(packet) match { case Successful(payload) => outQueue.enqueue((packet, payload)) - case Failure(cause) => log.error(cause.message) + case Failure(cause) => log.error(s"Could not encode $packet: ${cause.message}") } } } @@ -615,7 +612,7 @@ class MiddlewareActor( outQueueBundled.enqueue(smp(slot = 0, data.bytes)) sendFirstBundle() case Failure(cause) => - log.error(cause.message) + log.error(s"could not bundle $bundle: ${cause.message}") //to avoid packets being lost, unwrap bundle and queue the packets individually bundle.foreach { packet => outQueueBundled.enqueue(smp(slot = 0, packet.bytes)) @@ -626,7 +623,7 @@ class MiddlewareActor( } } catch { case e: Throwable => - log.error(s"outbound queue processing error - ${Option(e.getMessage).getOrElse(e.getClass.getSimpleName)}") + log.error(s"Outbound queue processing error: ${Option(e.getMessage).getOrElse(e.getClass.getSimpleName)}") } } @@ -901,7 +898,7 @@ class MiddlewareActor( case Successful(data) => data.grouped((MTU - 8) * 8).map(vec => smp(slot = 4, vec.bytes)).toSeq case Failure(cause) => - log.error(cause.message) + log.error(s"Could not split packet: ${cause.message}") Seq() } } else { diff --git a/src/main/scala/net/psforever/actors/net/SocketActor.scala b/src/main/scala/net/psforever/actors/net/SocketActor.scala index 65155e3a..06ccf77d 100644 --- a/src/main/scala/net/psforever/actors/net/SocketActor.scala +++ b/src/main/scala/net/psforever/actors/net/SocketActor.scala @@ -82,7 +82,7 @@ object SocketActor { socketActor ! toSocket(message) } } else { - log.info("Network simulator dropped packet") + log.trace("Network simulator dropped packet") } } diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 8143bfc0..38fecd82 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -47,7 +47,7 @@ import net.psforever.packet.game.{ PlanetsideAttributeMessage } import net.psforever.types.{ - CharacterGender, + CharacterSex, CharacterVoice, ExoSuitType, ImplantType, @@ -98,7 +98,7 @@ object AvatarActor { name: String, head: Int, voice: CharacterVoice.Value, - gender: CharacterGender.Value, + gender: CharacterSex, empire: PlanetSideEmpire.Value ) extends Command @@ -306,7 +306,7 @@ class AvatarActor( _.factionId -> lift(empire.id), _.headId -> lift(head), _.voiceId -> lift(voice.id), - _.genderId -> lift(gender.id), + _.genderId -> lift(gender.value), _.bep -> lift(Config.app.game.newAvatar.br.experience), _.cep -> lift(Config.app.game.newAvatar.cr.experience) ) @@ -326,7 +326,7 @@ class AvatarActor( result.onComplete { case Success(_) => - log.debug(s"created character ${name} for account ${account.name}") + log.debug(s"AvatarActor: created character ${name} for account ${account.name}") sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass) sendAvatars(account) case Failure(e) => log.error(e)("db failure") @@ -353,7 +353,7 @@ class AvatarActor( result.onComplete { case Success(_) => - log.debug(s"avatar $id deleted") + log.debug(s"AvatarActor: avatar $id deleted") sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass) sendAvatars(account) case Failure(e) => log.error(e)("db failure") @@ -485,14 +485,20 @@ class AvatarActor( ItemTransactionResultMessage(terminalGuid, TransactionType.Learn, success = false) ) } else { - - val deps = Certification.values.filter(_.requires.contains(certification)).toSet - val remove = deps ++ Certification.values.filter(_.replaces.intersect(deps).nonEmpty).toSet + certification + var requiredByCert: Set[Certification] = Set(certification) + var removeThese: Set[Certification] = Set(certification) + val allCerts: Set[Certification] = Certification.values.toSet + do { + removeThese = allCerts.filter { testingCert => + testingCert.requires.intersect(removeThese).nonEmpty + } + requiredByCert = requiredByCert ++ removeThese + } while(removeThese.nonEmpty) Future .sequence( avatar.certifications - .intersect(remove) + .intersect(requiredByCert) .map(cert => { ctx .run( @@ -511,7 +517,7 @@ class AvatarActor( ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false) ) case Success(certs) => - context.self ! ReplaceAvatar(avatar.copy(certifications = avatar.certifications.diff(remove))) + context.self ! ReplaceAvatar(avatar.copy(certifications = avatar.certifications.diff(certs))) certs.foreach { cert => sessionActor ! SessionActor.SendResponse( PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value) @@ -1269,7 +1275,7 @@ class AvatarActor( .run(query[persistence.Loadout].filter(_.avatarId == lift(avatar.id))) .map { loadouts => loadouts.map { loadout => - val doll = new Player(Avatar(0, "doll", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val doll = new Player(Avatar(0, "doll", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) doll.ExoSuit = ExoSuitType(loadout.exosuitId) loadout.items.split("/").foreach { diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala index a679a915..a73e15b4 100644 --- a/src/main/scala/net/psforever/actors/session/ChatActor.scala +++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala @@ -121,8 +121,6 @@ class ChatActor( Behaviors.same case Message(message) => - log.info("Chat: " + message) - val gmCommandAllowed = session.account.gm || Config.app.development.unprivilegedGmCommands.contains(message.messageType) @@ -675,11 +673,11 @@ class ChatActor( case (CMT_WARP, _, contents) if gmCommandAllowed => val buffer = contents.toLowerCase.split("\\s+") val (coordinates, waypoint) = (buffer.lift(0), buffer.lift(1), buffer.lift(2)) match { - case (Some(x), Some(y), Some(z)) => (Some(x, y, z), None) - case (Some("to"), Some(character), None) => (None, None) // TODO not implemented - case (Some("near"), Some(objectName), None) => (None, None) // TODO not implemented - case (Some(waypoint), None, None) => (None, Some(waypoint)) - case _ => (None, None) + case (Some(x), Some(y), Some(z)) => (Some(x, y, z), None) + case (Some("to"), Some(character), None) => (None, None) // TODO not implemented + case (Some("near"), Some(objectName), None) => (None, None) // TODO not implemented + case (Some(waypoint), None, None) if waypoint.nonEmpty => (None, Some(waypoint)) + case _ => (None, None) } (coordinates, waypoint) match { case (Some((x, y, z)), None) if List(x, y, z).forall { str => @@ -687,6 +685,12 @@ class ChatActor( coordinate.isDefined && coordinate.get >= 0 && coordinate.get <= 8191 } => sessionActor ! SessionActor.SetPosition(Vector3(x.toFloat, y.toFloat, z.toFloat)) + case (None, Some(waypoint)) if waypoint == "-list" => + val zone = PointOfInterest.get(session.player.Zone.id) + zone match { + case Some(zone: PointOfInterest) => sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, true, "", PointOfInterest.listAll(zone), None)) + case _ => ChatMsg(UNK_229, true, "", s"unknown player zone '${session.player.Zone.id}'", None) + } case (None, Some(waypoint)) if waypoint != "-help" => PointOfInterest.getWarpLocation(session.zone.id, waypoint) match { case Some(location) => sessionActor ! SessionActor.SetPosition(location) @@ -912,7 +916,7 @@ class ChatActor( } case _ => - log.info(s"unhandled chat message $message") + log.warn(s"Unhandled chat message $message") } Behaviors.same @@ -941,7 +945,7 @@ class ChatActor( val args = message.contents.split(" ") val (name, time) = (args.lift(0), args.lift(1)) match { case (Some(name), _) if name != session.player.Name => - log.error("received silence message for other player") + log.error("Received silence message for other player") (None, None) case (Some(name), None) => (Some(name), Some(5)) case (Some(name), Some(time)) if time.toIntOption.isDefined => (Some(name), Some(time.toInt)) @@ -972,11 +976,11 @@ class ChatActor( } case (name, time) => - log.error(s"bad silence args $name $time") + log.warn(s"Bad silence args $name $time") } case _ => - log.error(s"unexpected messageType $message") + log.warn(s"Unexpected messageType $message") } Behaviors.same diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 11b81180..4da6c37a 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -61,8 +61,9 @@ import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMes import net.psforever.services.properties.PropertyOverrideManager import net.psforever.services.support.SupportActor import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} +import net.psforever.services.hart.HartTimer import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} -import net.psforever.services.{InterstellarClusterService, RemoverActor, Service, ServiceManager} +import net.psforever.services.{RemoverActor, Service, ServiceManager, InterstellarClusterService => ICS} import net.psforever.types._ import net.psforever.util.{Config, DefinitionUtil} import net.psforever.zones.Zones @@ -173,7 +174,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var galaxyService: ActorRef = ActorRef.noSender var squadService: ActorRef = ActorRef.noSender var propertyOverrideManager: ActorRef = Actor.noSender - var cluster: typed.ActorRef[InterstellarClusterService.Command] = Actor.noSender + var cluster: typed.ActorRef[ICS.Command] = Actor.noSender var _session: Session = Session() var progressBarValue: Option[Float] = None var shooting: Option[PlanetSideGUID] = None //ChangeFireStateMessage_Start @@ -238,9 +239,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /** Upstream message counter
* Checks for server acknowledgement of the following messages in the following conditions:
* `PlayerStateMessageUpstream` (infantry)
- * `VehicleStateMessage` (driver seat only)
- * `ChildObjectStateMessage` (any gunner seat that is not the driver)
- * `KeepAliveMessage` (any passenger seat that is not the driver)
+ * `VehicleStateMessage` (driver mount only)
+ * `ChildObjectStateMessage` (any gunner mount that is not the driver)
+ * `KeepAliveMessage` (any passenger mount that is not the driver)
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second */ var upstreamMessageCount: Int = 0 @@ -298,7 +299,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con serviceManager ! Lookup("propertyOverrideManager") ServiceManager.receptionist ! Receptionist.Find( - InterstellarClusterService.InterstellarClusterServiceKey, + ICS.InterstellarClusterServiceKey, context.self ) @@ -348,17 +349,22 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con out case None => //delete stale entity reference from client - log.warn(s"${player.Name} has an invalid reference to GUID ${id.get} in zone ${continent.id}") + log.warn(s"ValidObject - ${player.Name} has an invalid GUID ${id.get.guid}, believing it in ${player.Sex.possessive} locker") sendResponse(ObjectDeleteMessage(id.get, 0)) None } + case Some(obj) if obj.HasGUID && obj.GUID != id.get => + log.error(s"ValidObject: ${player.Name} found an object that isn't the one ${player.Sex.pronounSubject} thought it was in zone ${continent.id}") + log.debug(s"ValidObject: potentially fatal error in ${continent.id} - requested ${id.get}, got ${obj.Definition.Name} with ${obj.GUID}; GUID mismatch") + None + case out @ Some(obj) if obj.HasGUID => out case None if id.nonEmpty && id.get != PlanetSideGUID(0) => //delete stale entity reference from client - log.warn(s"${player.Name} has an invalid reference to GUID ${id.get} in zone ${continent.id}") + log.error(s"${player.Name} has an invalid reference to GUID ${id.get.guid} in zone ${continent.id}") sendResponse(ObjectDeleteMessage(id.get, 0)) None @@ -369,21 +375,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def receive: Receive = { case LookupResult("accountIntermediary", endpoint) => accountIntermediary = endpoint - log.info("ID: " + session.id + " Got account intermediary service " + endpoint) case LookupResult("accountPersistence", endpoint) => accountPersistence = endpoint - log.info("ID: " + session.id + " Got account persistence service " + endpoint) case LookupResult("galaxy", endpoint) => galaxyService = endpoint - log.info("ID: " + session.id + " Got galaxy service " + endpoint) case LookupResult("squad", endpoint) => squadService = endpoint - log.info("ID: " + session.id + " Got squad service " + endpoint) case LookupResult("propertyOverrideManager", endpoint) => propertyOverrideManager = endpoint - log.info("ID: " + session.id + " Got propertyOverrideManager service " + endpoint) - case InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings) => + case ICS.InterstellarClusterServiceKey.Listing(listings) => cluster = listings.head // Avatar subscription update @@ -406,7 +407,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case AvatarActor.AvatarLoginResponse(avatar) => session = session.copy(avatar = avatar) Deployables.InitializeDeployableQuantities(avatar) - cluster ! InterstellarClusterService.FilterZones(_ => true, context.self) + cluster ! ICS.FilterZones(_ => true, context.self) case packet: PlanetSideGamePacket => handleGamePkt(packet) @@ -423,8 +424,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case SetSpeed(speed) => session = session.copy(speed = speed) - case SetFlying(flying) => - session = session.copy(flying = flying) + case SetFlying(_flying) => + session = session.copy(flying = _flying) case SetSpectator(spectator) => session.player.spectator = spectator @@ -434,7 +435,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con zoningChatMessageType = ChatMessageType.CMT_RECALL zoningStatus = Zoning.Status.Request beginZoningCountdown(() => { - cluster ! InterstellarClusterService.GetRandomSpawnPoint( + cluster ! ICS.GetRandomSpawnPoint( Zones.sanctuaryZoneNumber(player.Faction), player.Faction, Seq(SpawnGroup.Sanctuary), @@ -449,24 +450,24 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /* TODO no ask or adapters from classic to typed so this logic is happening in SpawnPointResponse implicit val timeout = Timeout(1 seconds) val future = - ask(cluster.toClassic, InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self)) - .mapTo[InterstellarClusterService.SpawnPointResponse] + ask(cluster.toClassic, ICS.GetInstantActionSpawnPoint(player.Faction, context.self)) + .mapTo[ICS.SpawnPointResponse] Await.result(future, 2 second) match { - case InterstellarClusterService.SpawnPointResponse(None) => + case ICS.SpawnPointResponse(None) => sendResponse( ChatMsg(ChatMessageType.CMT_INSTANTACTION, false, "", "@InstantActionNoHotspotsAvailable", None) ) - case InterstellarClusterService.SpawnPointResponse(Some(_)) => + case ICS.SpawnPointResponse(Some(_)) => beginZoningCountdown(() => { - cluster ! InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self) + cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) }) } beginZoningCountdown(() => { - cluster ! InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self) + cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) }) */ - cluster ! InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self) + cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) case Quit() => //priority to quitting is given to quit over other zoning methods @@ -477,7 +478,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con zoningChatMessageType = ChatMessageType.CMT_QUIT zoningStatus = Zoning.Status.Request beginZoningCountdown(() => { - log.info("Good-bye") + log.info(s"Good-bye, ${player.Name}") ImmediateDisconnect() }) @@ -559,37 +560,32 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest) => (manifest.passengers.find { case (name, _) => player.Name.equals(name) } match { - case Some((name, index)) if vehicle.Seats(index).Occupant.isEmpty => - vehicle.Seats(index).Occupant = player + case Some((name, index)) if vehicle.Seats(index).occupant.isEmpty => + vehicle.Seats(index).mount(player) Some(vehicle) case Some((name, index)) => - log.warn(s"TransferPassenger: seat $index is already occupied") + log.warn(s"TransferPassenger: $player tried to mount seat $index when it was already occupied, and was rebuked") None case None => None }).orElse(manifest.cargo.find { case (name, _) => player.Name.equals(name) } match { case Some((name, index)) => - vehicle.CargoHolds(index).Occupant match { + vehicle.CargoHolds(index).occupant match { case Some(cargo) => - cargo.Seats(0).Occupant match { - case Some(driver) if driver.Name.equals(name) => - Some(cargo) - case _ => - None - } + cargo.Seats(0).occupants.find(_.Name.equals(name)) case None => None } case None => None }) match { - case Some(v) => + case Some(v: Vehicle) => galaxyService ! Service.Leave(Some(temp_channel)) //temporary vehicle-specific channel (see above) deadState = DeadState.Release sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds) - case None => + case _ => interstellarFerry match { case None => galaxyService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones @@ -715,7 +711,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ourMember.ZoneId, unk7 = 0 ) - ) //repeat of our entry + ) val playerGuid = player.GUID //turn lfs off val factionChannel = s"${player.Faction}" @@ -920,18 +916,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Deployment.CanDeploy(obj, state) => if (state == DriveState.Deploying) { - log.info(s"DeployRequest: $obj transitioning to deploy state") + log.trace(s"DeployRequest: $obj transitioning to deploy state") } else if (state == DriveState.Deployed) { - log.info(s"DeployRequest: $obj has been Deployed") + log.trace(s"DeployRequest: $obj has been Deployed") } else { CanNotChangeDeployment(obj, state, "incorrect deploy state") } case Deployment.CanUndeploy(obj, state) => if (state == DriveState.Undeploying) { - log.info(s"DeployRequest: $obj transitioning to undeploy state") + log.trace(s"DeployRequest: $obj transitioning to undeploy state") } else if (state == DriveState.Mobile) { - log.info(s"DeployRequest: $obj is Mobile") + log.trace(s"DeployRequest: $obj is Mobile") } else { CanNotChangeDeployment(obj, state, "incorrect undeploy state") } @@ -944,7 +940,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case Zone.Population.PlayerHasLeft(zone, None) => - log.info(s"${avatar.name} does not have a body on ${zone.id}") + log.trace(s"PlayerHasLeft: ${avatar.name} does not have a body on ${zone.id}") case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) => if (tplayer.isAlive) { @@ -955,20 +951,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.warn(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?") case Zone.Population.PlayerAlreadySpawned(zone, tplayer) => - log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; a clerical error?") + log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?") - case InterstellarClusterService.SpawnPointResponse(response) => + case ICS.SpawnPointResponse(response) => zoningType match { case Zoning.Method.InstantAction if response.isEmpty => CancelZoningProcessWithReason("@InstantActionNoHotspotsAvailable") case Zoning.Method.InstantAction if zoningStatus == Zoning.Status.Request => beginZoningCountdown(() => { - cluster ! InterstellarClusterService.GetInstantActionSpawnPoint(player.Faction, context.self) + cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) }) - case zoningType => - val currentZoningType = zoningType + case ztype => + if (ztype != Zoning.Method.None) { + log.warn(s"SpawnPointResponse: ${player.Name}'s zoning was not in order at the time a response was received; attempting to guess what ${player.Sex.pronounSubject} wants to do") + } + val previousZoningType = zoningType CancelZoningProcess() PlayerActionsToCancel() CancelAllProximityUnits() @@ -983,22 +982,32 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player } val (pos, ori) = spawnPoint.SpecificPoint(obj) - if (zoningType == Zoning.Method.InstantAction) - LoadZonePhysicalSpawnPoint(zone.id, pos, ori, 0 seconds) + if (previousZoningType == Zoning.Method.InstantAction) + LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds) else LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id)) case None => - log.error("got None spawn point response from InterstellarClusterService") - Thread.sleep(1000) // throttle in case of infinite loop - RequestSanctuaryZoneSpawn(player, 0) + log.warn(s"SpawnPointResponse: ${player.Name} received no spawn point response when asking InterstellarClusterService; sending home") + //Thread.sleep(1000) // throttle in case of infinite loop + RequestSanctuaryZoneSpawn(player, currentZone = 0) } } + case ICS.DroppodLaunchDenial(errorCode, _) => + sendResponse(DroppodLaunchResponseMessage(errorCode, player.GUID)) + + case ICS.DroppodLaunchConfirmation(zone, position) => + LoadZoneLaunchDroppod(zone, position) + + case msg @ Zone.Vehicle.HasSpawned(zone, vehicle) => ; + case msg @ Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) => - log.warn(s"$msg") + log.warn(s"${player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason") + + case msg @ Zone.Vehicle.HasDespawned(zone, vehicle) => ; case msg @ Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) => - log.warn(s"$msg") + log.warn(s"${player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason") case Zone.Deployable.DeployableIsBuilt(obj, tool) => val index = player.Find(tool) match { @@ -1022,7 +1031,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case GlobalDefinitions.router_telepad => ; case _ => log.warn( - s"Zone.Deployable.DeployableIsBuilt: not sure what kind of construction item to animate - ${tool.Definition}" + s"Zone.Deployable.DeployableIsBuilt: not sure what kind of construction item to animate - ${tool.Definition.Name}" ) } import scala.concurrent.ExecutionContext.Implicits.global @@ -1096,7 +1105,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con RemoverActor.AddTask(obj, continent, Some(0 seconds)) ) } else { - log.info(s"FinalizeDeployable: setup for telepad #${guid.guid} in zone ${continent.id}") + log.debug(s"FinalizeDeployable: setup for telepad #${guid.guid} in zone ${continent.id}") obj.Router = routerGUID //necessary; forwards link to the router DeployableBuildActivity(obj) RemoveOldEquipmentFromInventory(player)(tool) @@ -1119,9 +1128,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(GenericObjectActionMessage(guid, 21)) //reset build cooldown sendResponse(ObjectDeployedMessage.Failure(definition.Name)) log.warn( - s"FinalizeDeployable: deployable ${definition.asInstanceOf[BaseDeployableDefinition].Item}@$guid not handled by specific case" + s"FinalizeDeployable: deployable ${definition.Item}@$guid not handled by specific case" ) - log.warn(s"FinalizeDeployable: deployable will be cleaned up, but may not get unregistered properly") + log.warn(s"FinalizeDeployable: deployable ${definition.Item}@$guid will be cleaned up, but may not get unregistered properly") TryDropFDU(tool, index, obj.Position) obj.Position = Vector3.Zero continent.Deployables ! Zone.Deployable.Dismiss(obj) @@ -1134,7 +1143,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Zone.Deployable.DeployableIsDismissed(obj) => continent.tasks ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) - case InterstellarClusterService.ZonesResponse(zones) => + case ICS.ZonesResponse(zones) => zones.foreach { zone => val continentNumber = zone.Number val popBO = 0 @@ -1197,7 +1206,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Zone.Nowhere => RequestSanctuaryZoneSpawn(player, currentZone = 0) case zone => - log.info(s"Zone ${zone.id} will now load") + log.trace(s"ZoneResponse: zone ${zone.id} will now load for ${player.Name}") loadConfZone = true val oldZone = continent session = session.copy(zone = zone) @@ -1211,7 +1220,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con self ! NewPlayerLoaded(player) } else { zoneReload = true - cluster ! InterstellarClusterService.GetNearbySpawnPoint( + cluster ! ICS.GetNearbySpawnPoint( continent.Number, player, Seq(SpawnGroup.Facility, SpawnGroup.Tower), @@ -1220,8 +1229,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - case InterstellarClusterService.ZoneResponse(zone) => - log.info(s"Zone ${zone.get.id} will now load") + case ICS.ZoneResponse(zone) => + log.trace(s"ZoneResponse: zone ${zone.get.id} will now load for ${player.Name}") loadConfZone = true val oldZone = session.zone session = session.copy(zone = zone.get) @@ -1242,7 +1251,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case NewPlayerLoaded(tplayer) => //new zone - log.info(s"Player ${tplayer.Name} has been loaded") + log.info(s"${tplayer.Name} has spawned into ${session.zone.id}") tplayer.avatar = avatar session = session.copy(player = tplayer) avatarActor ! AvatarActor.CreateImplants() @@ -1299,7 +1308,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * The user is either already in the current zone and merely transporting himself from one location to another, + * The user is either already in the current zone and merely transporting from one location to another, * also called "dying", or occasionally "deconstructing," * or is completely switching in between zones. * These correspond to the message `NewPlayerLoaded` for the case of "dying" or the latter zone switching case, @@ -1321,21 +1330,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val waitingOnUpstream = upstreamMessageCount == 0 if (attempt >= max_attempts && waitingOnUpstream) { log.warn( - s"SetCurrentAvatar-max attempt failure: " + + s"SetCurrentAvatar/${player.Name}: max attempt failure: " + s"zone=${if (zoneLoaded.contains(true)) "loaded" else if (zoneLoaded.contains(false)) "failed" - else "unloaded"}," + - s"guid=${tplayer.HasGUID}, control=${(tplayer.Actor != Default.Actor)}, avatar=$waitingOnUpstream" + else "unloaded"}, " + + s"guid=${tplayer.HasGUID}, control=${tplayer.Actor != Default.Actor}, no upstream messaging" ) zoneLoaded match { case None | Some(false) => log.warn( - "SetCurrentAvatar-max attempt failure: failed to load intended destination zone; routing to faction sanctuary" + s"SetCurrentAvatar/${player.Name}: max attempt failure: failed to load intended destination zone; routing to faction sanctuary" ) RequestSanctuaryZoneSpawn(tplayer, continent.Number) case _ => log.warn( - "SetCurrentAvatar-max attempt failure: the zone loaded but elements remain unready; restarting the process ..." + s"SetCurrentAvatar/${player.Name}: max attempt failure: the zone loaded but elements remain unready; restarting the process ..." ) val pos = shiftPosition.getOrElse(player.Position) val orient = shiftOrientation.getOrElse(player.Orientation) @@ -1385,14 +1394,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case Vitality.DamageResolution(target: PlanetSideGameObject, _) => - log.warn(s"Vital target ${target.Definition.Name} damage resolution not supported using this method") + log.warn(s"DamageResolution: vital target ${target.Definition.Name} damage resolution not supported") case ResponseToSelf(pkt) => - //log.info(s"Received a direct message: $pkt") sendResponse(pkt) case ReceiveAccountData(account) => - log.info(s"ReceiveAccountData ${account}") + log.trace(s"ReceiveAccountData $account") session = session.copy(account = account) avatarActor ! AvatarActor.SetAccount(account) @@ -1437,7 +1445,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con deadState = DeadState.RespawnTime session = session.copy(player = new Player(avatar)) - //xy-coordinates indicate sanctuary spawn bias: + //ay-coordinates indicate sanctuary spawn bias: player.Position = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 4) match { case 0 => Vector3(8192, 8192, 0) //NE case 1 => Vector3(8192, 0, 0) //SE @@ -1503,12 +1511,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _ => //fall back to sanctuary/prior? - log.error(s"LoginInfo: player $playerName could not be found in game world") + log.info(s"LoginInfo: player $playerName could not be found in game world") self.forward(PlayerToken.LoginInfo(playerName, Zone.Nowhere, pos)) } case PlayerToken.CanNotLogin(playerName, reason) => - log.warn(s"LoginInfo: player $playerName is denied login for reason: $reason") + log.warn(s"LoginInfo: $playerName is denied login for reason - $reason") reason match { case PlayerToken.DeniedLoginReason.Kicked => KickedByAdministration() case _ => sendResponse(DisconnectMessage("You will be logged out.")) @@ -1520,10 +1528,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con _: Int, _: Option[Equipment] ) => - log.info(s"$msg") + log.debug(s"ItemPutInSlot: $msg") case msg @ Containable.CanNotPutItemInSlot(_: PlanetSideServerObject with Container, _: Equipment, _: Int) => - log.info(s"$msg") + log.debug(s"CanNotPutItemInSlot: $msg") case default => log.warn(s"Invalid packet class received: $default from ${sender()}") @@ -1624,55 +1632,30 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * You can't instant action to respond to some activity using a droppod! - * You can't. - * You just can't. + * Attach the player to a droppod vehicle and hurtle them through the stratosphere in some far off world. + * Perform all normal operation standardization (state cancels) as if any of form of zoning was being performed, + * then assemble the vehicle and work around some inconvenient setup requirements for vehicle gating. + * You can't instant action to respond to some activity using a droppod. * @param zone the destination zone - * @param hotspotPosition where is the hotspot that is being addressed - * @param spawnPosition the destination spawn position (may not be related to a literal `SpawnPoint` entity) + * @param spawnPosition the destination drop position */ - def YouCantInstantActionUsingDroppod(zone: Zone, hotspotPosition: Vector3, spawnPosition: Vector3): Unit = { + def LoadZoneLaunchDroppod(zone: Zone, spawnPosition: Vector3): Unit = { + log.info(s"${player.Name} is launching to ${zone.id} in ${player.Sex.possessive} droppod") CancelZoningProcess() PlayerActionsToCancel() CancelAllProximityUnits() - //find a safe drop point - var targetBuildings = zone.Buildings.values - var whereToDroppod = spawnPosition.xy - while (targetBuildings.nonEmpty) { - (targetBuildings - .filter { building => - val radius = building.Definition.SOIRadius - Vector3.DistanceSquared(building.Position.xy, whereToDroppod) < radius * radius - }) match { - case Nil => - //no soi interference - targetBuildings = Nil - case List(building: Building) => - //blocked by a single soi; find space just outside of this soi and confirm no new overlap - val radius = Vector3(0, building.Definition.SOIRadius.toFloat + 5f, 0) - whereToDroppod = - building.Position.xy + Vector3.Rz(radius, math.abs(scala.util.Random.nextInt() % 360).toFloat) - case buildings => - //probably blocked by a facility and its tower (maximum overlap potential is 2?); find space outside of largest soi - val largestBuilding = buildings.maxBy(_.Definition.SOIRadius) - val radius = Vector3(0, largestBuilding.Definition.SOIRadius.toFloat + 5f, 0) - whereToDroppod = - largestBuilding.Position.xy + Vector3.Rz(radius, math.abs(scala.util.Random.nextInt() % 360).toFloat) - targetBuildings = buildings - } - } //droppod action val droppod = Vehicle(GlobalDefinitions.droppod) droppod.Faction = player.Faction - droppod.Position = whereToDroppod.xy + Vector3.z(1024) + 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).Occupant = player + 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 + player.VehicleSeated = PlanetSideGUID(0) LoadZonePhysicalSpawnPoint(zone.id, droppod.Position, Vector3.Zero, 0 seconds) - /* Don't even think about it. */ } /** @@ -1723,7 +1706,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con else PlanetSideGUID(0) reply match { case AvatarResponse.TeardownConnection() => - log.info("ending session by event system request (relog)") + log.trace(s"ending ${player.Name}'s old session by event system request (relog)") context.stop(self) case AvatarResponse.SendResponse(msg) => @@ -1736,6 +1719,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case AvatarResponse.Revive(target_guid) => if (tplayer_guid == target_guid) { + log.info(s"No time for rest, ${player.Name}. Back on your feet!") reviveTimer.cancel() deadState = DeadState.Alive player.Revive @@ -1862,7 +1846,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con reviveTimer.cancel() if (player.death_by == 0) { reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) { - cluster ! InterstellarClusterService.GetRandomSpawnPoint( + cluster ! ICS.GetRandomSpawnPoint( Zones.sanctuaryZoneNumber(player.Faction), player.Faction, Seq(SpawnGroup.Sanctuary), @@ -2021,15 +2005,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case AvatarResponse.WeaponDryFire(weapon_guid) => if (tplayer_guid != guid) { - // Check that the magazine is still empty before sending WeaponDryFireMessage - // As it could have been reloaded since the packet was dispatched, which would make other clients not see it firing continent.GUID(weapon_guid) match { - case Some(tool: Tool) => { + case Some(tool: Tool) => + // check that the magazine is still empty before sending WeaponDryFireMessage + // if it has been reloaded since then, other clients not see it firing if (tool.Magazine == 0) { sendResponse(WeaponDryFireMessage(weapon_guid)) } - } - case _ => log.warn(s"Tried to send WeaponDryFire but GUID ${weapon_guid} does not seem to be a Tool") + case Some(_) => + sendResponse(WeaponDryFireMessage(weapon_guid)) + case None => ; } } @@ -2186,7 +2171,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /** * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays. - * Intended to assist in sanitizing loadout information from the perspectvie of the player, or target owner. + * Intended to assist in sanitizing loadout information from the perspective of the player, or target owner. * The equipment is expected to be unregistered and already fitted to their ultimate slot in the target container. * @param player the player whose purchasing constraints are to be tested * @param target the location in which the equipment will be stowed @@ -2220,6 +2205,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (player.HasGUID) player.GUID else PlanetSideGUID(0) reply match { + case LocalResponse.AlertDestroyDeployable(obj: BoomerDeployable) => + //the (former) owner (obj.OwnerName) should process this message + obj.Trigger match { + case Some(item: BoomerTrigger) => + FindEquipmentToDelete(item.GUID, item) + item.Companion = None + case _ => ; + } + avatar.deployables.Remove(obj) + UpdateDeployableUIElements(avatar.deployables.UpdateUIElement(obj.Definition.Item)) + case LocalResponse.AlertDestroyDeployable(obj) => //the (former) owner (obj.OwnerName) should process this message avatar.deployables.Remove(obj) @@ -2230,17 +2226,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) } - case LocalResponse.Detonate(guid, obj: BoomerDeployable) => - sendResponse(TriggerEffectMessage(guid, "detonate_boomer")) - sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) - sendResponse(ObjectDeleteMessage(guid, 0)) + case LocalResponse.Detonate(dguid, obj: BoomerDeployable) => + sendResponse(TriggerEffectMessage(dguid, "detonate_boomer")) + sendResponse(PlanetsideAttributeMessage(dguid, 29, 1)) + sendResponse(ObjectDeleteMessage(dguid, 0)) - case LocalResponse.Detonate(guid, obj: ExplosiveDeployable) => - sendResponse(GenericObjectActionMessage(guid, 19)) - sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) - sendResponse(ObjectDeleteMessage(guid, 0)) + case LocalResponse.Detonate(dguid, obj: ExplosiveDeployable) => + sendResponse(GenericObjectActionMessage(dguid, 19)) + sendResponse(PlanetsideAttributeMessage(dguid, 29, 1)) + sendResponse(ObjectDeleteMessage(dguid, 0)) - case LocalResponse.Detonate(guid, obj) => + case LocalResponse.Detonate(_, obj) => log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly") case LocalResponse.DoorOpens(door_guid) => @@ -2251,14 +2247,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case LocalResponse.DoorCloses(door_guid) => //door closes for everyone sendResponse(GenericObjectStateMsg(door_guid, 17)) - case LocalResponse.EliminateDeployable(obj: TurretDeployable, guid, pos) => + case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos) => if (obj.Destroyed) { - DeconstructDeployable(obj, guid, pos) + DeconstructDeployable(obj, dguid, pos) } else { obj.Destroyed = true DeconstructDeployable( obj, - guid, + dguid, pos, obj.Orientation, if (obj.MountPoints.isEmpty) 2 @@ -2266,28 +2262,28 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } - case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, guid, pos) => + case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos) => if (obj.Destroyed || obj.Jammed || obj.Health == 0) { - DeconstructDeployable(obj, guid, pos) + DeconstructDeployable(obj, dguid, pos) } else { obj.Destroyed = true - DeconstructDeployable(obj, guid, pos, obj.Orientation, 2) + DeconstructDeployable(obj, dguid, pos, obj.Orientation, 2) } - case LocalResponse.EliminateDeployable(obj: ComplexDeployable, guid, pos) => + case LocalResponse.EliminateDeployable(obj: ComplexDeployable, dguid, pos) => if (obj.Destroyed) { - DeconstructDeployable(obj, guid, pos) + DeconstructDeployable(obj, dguid, pos) } else { obj.Destroyed = true - DeconstructDeployable(obj, guid, pos, obj.Orientation, 1) + DeconstructDeployable(obj, dguid, pos, obj.Orientation, 1) } - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, guid, pos) => + case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos) => //if active, deactivate if (obj.Active) { obj.Active = false - sendResponse(GenericObjectActionMessage(guid, 29)) - sendResponse(GenericObjectActionMessage(guid, 30)) + sendResponse(GenericObjectActionMessage(dguid, 29)) + sendResponse(GenericObjectActionMessage(dguid, 30)) } //determine if no replacement teleport system exists continent.GUID(obj.Router) match { @@ -2296,7 +2292,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if ( router.Utility(UtilityType.internal_router_telepad_deployable) match { case Some(internalTelepad: Utility.InternalTelepad) => - internalTelepad.Telepad.contains(guid) //same telepad + internalTelepad.Telepad.contains(dguid) //same telepad case _ => true } ) { @@ -2307,24 +2303,22 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } //standard deployable elimination behavior if (obj.Destroyed) { - DeconstructDeployable(obj, guid, pos) + DeconstructDeployable(obj, dguid, pos) } else { obj.Destroyed = true - DeconstructDeployable(obj, guid, pos, obj.Orientation, 2) + DeconstructDeployable(obj, dguid, pos, obj.Orientation, 2) } - case LocalResponse.EliminateDeployable(obj, guid, pos) => + case LocalResponse.EliminateDeployable(obj, dguid, pos) => if (obj.Destroyed) { - DeconstructDeployable(obj, guid, pos) + DeconstructDeployable(obj, dguid, pos) } else { obj.Destroyed = true - DeconstructDeployable(obj, guid, pos, obj.Orientation, 2) + DeconstructDeployable(obj, dguid, pos, obj.Orientation, 2) } case LocalResponse.SendHackMessageHackCleared(target_guid, unk1, unk2) => - log.trace(s"Clearing hack for ${target_guid}") - // Reset hack state for all players - sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)) + //log.trace(s"Clearing hack for $target_guid") case LocalResponse.HackObject(target_guid, unk1, unk2) => HackObject(target_guid, unk1, unk2) @@ -2387,6 +2381,26 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case LocalResponse.SetEmpire(object_guid, empire) => sendResponse(SetEmpireMessage(object_guid, empire)) + case LocalResponse.SendResponse(pkt) => + sendResponse(pkt) + + case LocalResponse.ShuttleEvent(ev) => + val msg = OrbitalShuttleTimeMsg( + ev.u1, ev.u2, + ev.t1, ev.t2, ev.t3, + ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) } + ) + sendResponse(msg) + + case LocalResponse.ShuttleDock(pguid, sguid, slot) => + sendResponse(ObjectAttachMessage(pguid, sguid, slot)) + + case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) => + sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient)) + + case LocalResponse.ShuttleState(sguid, pos, orient, state) => + sendResponse(VehicleStateMessage(sguid, 0, pos, orient, None, Some(state), 0, 0, 15, false, false)) + case LocalResponse.ToggleTeleportSystem(router, system_plan) => ToggleTeleportSystem(router, system_plan) @@ -2407,7 +2421,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case LocalResponse.RechargeVehicleWeapon(vehicle_guid, weapon_guid) => { if (tplayer_guid == guid) { continent.GUID(vehicle_guid) match { - case Some(vehicle: Mountable with MountedWeapons) => + case Some(vehicle: MountableWeapons) => vehicle.PassengerInSeat(player) match { case Some(seat_num: Int) => vehicle.WeaponControlledFromSeat(seat_num) match { @@ -2434,17 +2448,25 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def HandleMountMessages(tplayer: Player, reply: Mountable.Exchange): Unit = { reply match { - case Mountable.CanMount(obj: ImplantTerminalMech, seat_num) => + case Mountable.CanMount(obj: ImplantTerminalMech, seat_number, mount_point) => CancelZoningProcessWithDescriptiveReason("cancel_use") CancelAllProximityUnits() - MountingAction(tplayer, obj, seat_num) + MountingAction(tplayer, obj, seat_number) // the player will receive no messages consistently except the KeepAliveMessage echo keepAliveFunc = KeepAlivePersistence - case Mountable.CanMount(obj: Vehicle, seat_num) => + case Mountable.CanMount(obj: Vehicle, seat_number, _) + if obj.Definition == GlobalDefinitions.orbital_shuttle => + CancelZoningProcessWithDescriptiveReason("cancel_mount") + CancelAllProximityUnits() + MountingAction(tplayer, obj, seat_number) + // the player will receive no messages consistently except the KeepAliveMessage echo + keepAliveFunc = KeepAlivePersistence + + case Mountable.CanMount(obj: Vehicle, seat_number, _) => CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID - log.info(s"MountVehicleMsg: ${player.Name}_guid mounts $obj_guid @ $seat_num") + log.info(s"${player.Name} mounts ${obj.Definition.Name} in seat $seat_number") CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj_guid, 68, obj.Shields)) //shield health @@ -2455,29 +2477,30 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val capacitor = scala.math.ceil((obj.Capacitor.toFloat / obj.Definition.MaxCapacitor.toFloat) * 10).toInt sendResponse(PlanetsideAttributeMessage(obj_guid, 113, capacitor)) } - if (seat_num == 0) { + if (seat_number == 0) { if (obj.Definition == GlobalDefinitions.quadstealth) { //wraith cloak state matches the cloak state of the driver //phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks obj.Cloaked = tplayer.Cloaked } - } else if (obj.Seats(seat_num).ControlledWeapon.isEmpty) { + } else if (obj.WeaponControlledFromSeat(seat_number).isEmpty) { // the player will receive no messages consistently except the KeepAliveMessage echo keepAliveFunc = KeepAlivePersistence } AccessContainer(obj) - UpdateWeaponAtSeatPosition(obj, seat_num) - MountingAction(tplayer, obj, seat_num) + UpdateWeaponAtSeatPosition(obj, seat_number) + MountingAction(tplayer, obj, seat_number) - case Mountable.CanMount(obj: FacilityTurret, seat_num) => + case Mountable.CanMount(obj: FacilityTurret, seat_number, mount_point) => CancelZoningProcessWithDescriptiveReason("cancel_mount") if (!obj.isUpgrading) { + log.info(s"${player.Name} mounts ${obj.Definition.Name}") if (obj.Definition == GlobalDefinitions.vanu_sentry_turret) { obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction)) } sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) - UpdateWeaponAtSeatPosition(obj, seat_num) - MountingAction(tplayer, obj, seat_num) + UpdateWeaponAtSeatPosition(obj, seat_number) + MountingAction(tplayer, obj, seat_number) // the player will receive no messages consistently except the KeepAliveMessage echo keepAliveFunc = KeepAlivePersistence } else { @@ -2486,25 +2509,68 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } - case Mountable.CanMount(obj: PlanetSideGameObject with WeaponTurret, seat_num) => + case Mountable.CanMount(obj: PlanetSideGameObject with WeaponTurret, seat_number, mount_point) => CancelZoningProcessWithDescriptiveReason("cancel_mount") sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) - UpdateWeaponAtSeatPosition(obj, seat_num) - MountingAction(tplayer, obj, seat_num) + UpdateWeaponAtSeatPosition(obj, seat_number) + MountingAction(tplayer, obj, seat_number) // the player will receive no messages consistently except the KeepAliveMessage echo keepAliveFunc = KeepAlivePersistence - case Mountable.CanMount(obj: Mountable, _) => - log.warn(s"MountVehicleMsg: $obj is some generic mountable object and nothing will happen") + case Mountable.CanMount(obj: Mountable, _, _) => + log.warn(s"MountVehicleMsg: $obj is some mountable object and nothing will happen for ${player.Name}") - case Mountable.CanDismount(obj: ImplantTerminalMech, seat_num) => + case Mountable.CanDismount(obj: ImplantTerminalMech, seat_num, _) => DismountAction(tplayer, obj, seat_num) - case Mountable.CanDismount(obj: Vehicle, seat_num) if obj.Definition == GlobalDefinitions.droppod => + case Mountable.CanDismount(obj: Vehicle, seat_num, mount_point) + if obj.Definition == GlobalDefinitions.orbital_shuttle => + val pguid = player.GUID + if (obj.MountedIn.nonEmpty) { + //dismount to hart lobby + val sguid = obj.GUID + val (pos, zang) = Vehicles.dismountShuttle(obj, mount_point) + tplayer.Position = pos + sendResponse(DelayedPathMountMsg(pguid, sguid, 60, true)) + continent.LocalEvents ! LocalServiceMessage( + continent.id, + LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, 0, 0, zang)) + ) + } + else { + //get ready for orbital drop + DismountAction(tplayer, obj, seat_num) + log.info(s"${player.Name} is prepped for dropping") + //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages + continent.VehicleEvents ! VehicleServiceMessage( + player.Name, + VehicleAction.SendResponse(Service.defaultPlayerGUID, PlayerStasisMessage(pguid)) //the stasis message + ) + //when the player dismounts, they will be positioned where the shuttle was when it disappeared in the sky + //the player will fall to the ground and is perfectly vulnerable in this state + //additionally, our player must exist in the current zone + //having no in-game avatar target will throw us out of the map screen when deploying and cause softlock + continent.VehicleEvents ! VehicleServiceMessage( + player.Name, + VehicleAction.SendResponse( + Service.defaultPlayerGUID, + PlayerStateShiftMessage(ShiftState(0, obj.Position, obj.Orientation.z, None)) //cower in the shuttle bay + ) + ) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, 9)) //conceal the player + ) + } + keepAliveFunc = NormalKeepAlive + + case Mountable.CanDismount(obj: Vehicle, seat_num, _) + if obj.Definition == GlobalDefinitions.droppod => UnaccessContainer(obj) DismountAction(tplayer, obj, seat_num) + obj.Actor ! Vehicle.Deconstruct() - case Mountable.CanDismount(obj: Vehicle, seat_num) => + case Mountable.CanDismount(obj: Vehicle, seat_num, _) => val player_guid: PlanetSideGUID = tplayer.GUID if (player_guid == player.GUID) { //disembarking self @@ -2518,26 +2584,28 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } - case Mountable.CanDismount(obj: PlanetSideGameObject with WeaponTurret, seat_num) => + case Mountable.CanDismount(obj: PlanetSideGameObject with WeaponTurret, seat_num, _) => DismountAction(tplayer, obj, seat_num) - case Mountable.CanDismount(obj: Mountable, _) => - log.warn(s"DismountVehicleMsg: $obj is some generic mountable object and nothing will happen") + case Mountable.CanDismount(obj: Mountable, _, _) => + log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}") - case Mountable.CanNotMount(obj: Vehicle, seat_num) => - log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seat_num, but was not allowed") - if (obj.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver)) { - sendResponse( - ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None) - ) + case Mountable.CanNotMount(obj: Vehicle, mount_point) => + log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's mount $mount_point, but was not allowed") + obj.GetSeatFromMountPoint(mount_point) match { + case Some(seatNum) if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) => + sendResponse( + ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None) + ) + case _ => } - case Mountable.CanNotMount(obj: Mountable, seat_num) => - log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seat_num, but was not allowed") + case Mountable.CanNotMount(obj: Mountable, mount_point) => + log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's mount $mount_point, but was not allowed") case Mountable.CanNotDismount(obj, seat_num) => log.warn( - s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's seat $seat_num, but was not allowed" + s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seat_num, but was not allowed" ) } } @@ -2612,7 +2680,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con slot.Equipment = entry.obj case None => log.warn( - s"applying default loadout to $vehicle on spawn, but can not find a mounted weapon @ ${entry.start}" + s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}" ) } }) @@ -2635,11 +2703,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Terminal.NoDeal() => val order: String = if (msg == null) { - s"order $msg" - } else { "missing order" + } else { + s"${msg.transaction_type} order" } - log.warn(s"${tplayer.Name} made a request but the terminal rejected the $order") + log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the $order") sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false)) lastTerminalOrderFulfillment = true @@ -2713,8 +2781,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } - case VehicleResponse.KickPassenger(seat_num, wasKickedByDriver, vehicle_guid) => - // seat_num seems to be correct if passenger is kicked manually by driver, but always seems to return 4 if user is kicked by seat permissions + case VehicleResponse.KickPassenger(_, wasKickedByDriver, vehicle_guid) => + //seat number (first field) seems to be correct if passenger is kicked manually by driver + //but always seems to return 4 if user is kicked by mount permissions changing sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) player.VehicleSeated = None if (tplayer_guid == guid) { @@ -2857,7 +2926,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off sendResponse(PlanetsideAttributeMessage(player.GUID, 21, vehicle_guid)) //ownership - vehicle.Actor ! Mountable.TryMount(player, 0) + vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 } match { + case Some((mountPoint, _)) => vehicle.Actor ! Mountable.TryMount(player, mountPoint) + case _ => ; + } case VehicleResponse.PlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID @@ -2873,14 +2945,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2) case VehicleResponse.PeriodicReminder(cause, data) => - val msg: String = (cause match { + val msg: String = cause match { case VehicleSpawnPad.Reminders.Blocked => s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}" case VehicleSpawnPad.Reminders.Queue => - s"Your position in the vehicle spawn queue is ${data.getOrElse("last")}." + s"Your position in the vehicle spawn queue is ${data.getOrElse("dead last")}." case VehicleSpawnPad.Reminders.Cancelled => "Your vehicle order has been cancelled." - }) + } sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None)) case VehicleResponse.ChangeLoadout(target, old_weapons, added_weapons, old_inventory, new_inventory) => @@ -2890,23 +2962,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (player.avatar.vehicle.contains(target)) { //owner: must unregister old equipment, and register and install new equipment (old_weapons ++ old_inventory).foreach { - case (obj, guid) => - sendResponse(ObjectDeleteMessage(guid, 0)) + case (obj, eguid) => + sendResponse(ObjectDeleteMessage(eguid, 0)) continent.tasks ! GUIDTask.UnregisterEquipment(obj)(continent.GUID) } ApplyPurchaseTimersBeforePackingLoadout(player, vehicle, added_weapons ++ new_inventory) } else if (accessedContainer.contains(target)) { //external participant: observe changes to equipment - (old_weapons ++ old_inventory).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } + (old_weapons ++ old_inventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, 0)) } } vehicle.PassengerInSeat(player) match { case Some(seatNum) => //participant: observe changes to equipment - (old_weapons ++ old_inventory).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } + (old_weapons ++ old_inventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, 0)) } UpdateWeaponAtSeatPosition(vehicle, seatNum) case None => //observer: observe changes to external equipment - old_weapons.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) } + old_weapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, 0)) } } case _ => ; } @@ -3025,7 +3097,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param tplayer the target player */ def HandleSetCurrentAvatar(tplayer: Player): Unit = { - log.info(s"HandleSetCurrentAvatar - ${tplayer.Name}") + log.trace(s"HandleSetCurrentAvatar - ${tplayer.Name}") session = session.copy(player = tplayer) val guid = tplayer.GUID UpdateDeployableUIElements(Deployables.InitializeDeployableUIElements(avatar)) @@ -3045,8 +3117,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.Actor ! JammableUnit.ClearJammeredStatus() player.Actor ! JammableUnit.ClearJammeredSound() } - // TODO only when respawning after death - avatarActor ! AvatarActor.ResetImplants() + if (deadState != DeadState.Alive) { + avatarActor ! AvatarActor.ResetImplants() + } sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0)) //TODO if Medkit does not have shortcut, add to a free slot or write over slot 64 @@ -3075,6 +3148,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //TODO 30 for a new character only? sendResponse(AvatarStatisticsMessage(2, Statistics(0L))) }) + if (tplayer.ExoSuit == ExoSuitType.MAX) { + sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong)) + } //AvatarAwardMessage //DisplayAwardMessage sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name")) @@ -3104,14 +3180,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con avatarActor ! AvatarActor.SetVehicle(None) } GetVehicleAndSeat() match { - //we're falling case (Some(vehicle), _) if vehicle.Definition == GlobalDefinitions.droppod => + //we're falling sendResponse( DroppodFreefallingMessage( vehicle.GUID, - vehicle.Position + Vector3.z(50), - Vector3.z(-999), - vehicle.Position + Vector3.z(25), + vehicle.Position, + Vector3.z(value = -999), + vehicle.Position + Vector3(-20, 1.156f, -50), Vector3(0, 70.3125f, 90), Vector3(0, 0, 90) ) @@ -3169,7 +3245,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con /** * These messages are dispatched when first starting up the client and connecting to the server for the first time. - * While many of thee messages will be reused for other situations, they appear in this order only during startup. + * While many of these messages will be reused for other situations, they appear in this order only during startup. */ def FirstTimeSquadSetup(): Unit = { sendResponse(SquadDetailDefinitionUpdateMessage.Init) @@ -3269,8 +3345,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def handleGamePkt(pkt: PlanetSideGamePacket) = pkt match { case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) => - val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" - log.info(s"New world login to $server with Token:$token. $clientVersion") + log.trace(s"ConnectToWorldRequestMessage: client with versioning $majorVersion.$minorVersion.$revision, $buildDate has sent token $token to the server") sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, false, "", "", None)) import scala.concurrent.ExecutionContext.Implicits.global clientKeepAlive.cancel() @@ -3279,7 +3354,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con accountIntermediary ! RetrieveAccountData(token) case msg @ MountVehicleCargoMsg(player_guid, cargo_guid, carrier_guid, unk4) => - log.info(msg.toString) + log.debug(s"MountVehicleCargoMsg: $msg") (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match { case (Some(cargo: Vehicle), Some(carrier: Vehicle)) => carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match { @@ -3287,18 +3362,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con cargo.Actor ! CargoBehavior.CheckCargoMounting(carrier_guid, mountPoint, 0) case _ => log.warn( - s"MountVehicleCargoMsg: target carrier vehicle (${carrier.Definition.Name}) does not have a cargo hold" + s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold" ) } case (None, _) | (Some(_), None) => log.warn( - s"MountVehicleCargoMsg: one or more of the target vehicles do not exist - $carrier_guid or $cargo_guid" + s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid" ) case _ => ; } case msg @ DismountVehicleCargoMsg(player_guid, cargo_guid, bailed, requestedByPassenger, kicked) => - log.info(msg.toString) + 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 { @@ -3326,7 +3401,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con keepAliveFunc() case msg @ BeginZoningMessage() => - log.info("Reticulating splines ...") + log.trace(s"BeginZoningMessage: ${player.Name} is reticulating ${continent.id}'s splines ...") zoneLoaded = None val continentId = continent.id val faction = player.Faction @@ -3353,7 +3428,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con foundDeployables.foreach(obj => { if (avatar.deployables.Add(obj)) { obj.Owner = guid - log.info(s"Found a ${obj.Definition.Name} of ours while loading the zone") } }) //render deployable objects @@ -3385,7 +3459,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con .asInstanceOf[Mountable] .Seats .values - .map(_.Occupant) + .map(_.occupant) .collect { case Some(occupant) => if (occupant.isAlive) { @@ -3446,7 +3520,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con }) //load active players in zone (excepting players who are seated or players who are us) val live = continent.LivePlayers - log.info(s"loading players $live") live .filterNot(tplayer => { tplayer.GUID == player.GUID || tplayer.VehicleSeated.nonEmpty @@ -3481,11 +3554,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con _.GUID != vehicle.GUID } case Some(_) => - //vehicle, but we're not seated in it + log.warn(s"BeginZoningMessage: ${player.Name} thought ${player.Sex.pronounSubject} was sitting in a vehicle, but it just evaporated around ${player.Sex.pronounObject}") player.VehicleSeated = None (b, List.empty[Vehicle]) case None => - //throw error since VehicleSeated didn't point to a vehicle? player.VehicleSeated = None (b, List.empty[Vehicle]) } @@ -3501,10 +3573,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) //occupants other than driver vehicle.Seats - .filter({ case (index, seat) => seat.isOccupied && live.contains(seat.Occupant.get) && index > 0 }) + .filter({ case (index, seat) => seat.isOccupied && live.contains(seat.occupant.get) && index > 0 }) .foreach({ case (index, seat) => - val targetPlayer = seat.Occupant.get + val targetPlayer = seat.occupant.get val targetDefiniton = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3528,11 +3600,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con vehicle.Seats .filter({ case (index, seat) => - seat.isOccupied && !seat.Occupant.contains(player) && live.contains(seat.Occupant.get) && index > 0 + seat.isOccupied && !seat.occupant.contains(player) && live.contains(seat.occupant.get) && index > 0 }) .foreach({ case (index, seat) => - val targetPlayer = seat.Occupant.get + val targetPlayer = seat.occupant.get val targetDefinition = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3543,7 +3615,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) }) - //since we would have only subscribed recently, we need to reload seat access states + //since we would have only subscribed recently, we need to reload mount access states (0 to 3).foreach { group => sendResponse(PlanetsideAttributeMessage(vguid, group + 10, vehicle.PermissionGroup(group).get.id)) } @@ -3566,15 +3638,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //cargo occupants (including our own vehicle as cargo) allActiveVehicles.collect { case vehicle if vehicle.CargoHolds.nonEmpty => - vehicle.CargoHolds.collect({ - case (index, hold: Cargo) if hold.isOccupied => { + vehicle.CargoHolds.collect { + case (_index, hold: Cargo) if hold.isOccupied => CargoBehavior.CargoMountBehaviorForAll( vehicle, - hold.Occupant.get, - index + hold.occupant.get, + _index ) //CargoMountBehaviorForUs can fail to attach the cargo vehicle on some clients - } - }) + } } //special deploy states val deployedVehicles = allActiveVehicles.filter(_.DeploymentState == DriveState.Deployed) @@ -3599,6 +3670,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deployed, 0, false, Vector3.Zero)) ToggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) } + val name = avatar.name + serviceManager.ask(Lookup("hart"))(Timeout(2 seconds)) + .onComplete { + case Success(LookupResult("hart", ref)) => + ref ! HartTimer.Update(continentId, name) + case _ => + } //implant terminals continent.map.terminalToInterface.foreach({ @@ -3617,11 +3695,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) case _ => ; } - //seat terminal occupants + //mount terminal occupants continent.GUID(terminal_guid) match { case Some(obj: Mountable) => - obj.Seats(0).Occupant match { - case Some(targetPlayer) => + obj.Seats(0).occupant match { + case Some(targetPlayer: Player) => val targetDefinition = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3631,7 +3709,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con targetDefinition.Packet.ConstructorData(targetPlayer).get ) ) - case None => ; + case _ => ; } case _ => ; } @@ -3661,9 +3739,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } //reserved ammunition? //TODO need to register if it exists - //seat turret occupant - turret.Seats(0).Occupant match { - case Some(targetPlayer) => + //mount turret occupant + turret.Seats(0).occupant match { + case Some(targetPlayer: Player) => val targetDefinition = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3673,7 +3751,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con targetDefinition.Packet.ConstructorData(targetPlayer).get ) ) - case None => ; + case _ => ; } } continent.VehicleEvents ! VehicleServiceMessage( @@ -3699,7 +3777,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con unk5, unk6 ) => - //log.info(s"$msg") persist() turnCounterFunc(avatar_guid) val isMoving = WorldEntity.isMoving(vel) @@ -3707,6 +3784,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (isMovingPlus) { CancelZoningProcessWithDescriptiveReason("cancel_motion") } +// if (is_crouching && !player.Crouching) { +// //dev stuff goes here +// } player.Position = pos player.Velocity = vel player.Orientation = Vector3(player.Orientation.x, pitch, yaw) @@ -3726,7 +3806,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } accessedContainer match { - case Some(veh: Vehicle) => + // Ensure we don't unload the contents of the vehicle trunk for players seated in the vehicle. + // This can happen if PSUM arrives during the mounting process + case Some(veh: Vehicle) if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID => if ( isMoving || veh.isMoving(1) || Vector3.DistanceSquared( player.Position, @@ -3739,7 +3821,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con UnaccessContainer(veh) } case Some(container) => //just in case - if (isMovingPlus) { + if (isMovingPlus && (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID)) { // Ensure we don't close the container if the player is seated in it val guid = player.GUID // If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first. if (container.HasGUID) { @@ -3779,7 +3861,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.zoneInteraction() case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) => - //log.info(s"$msg") //the majority of the following check retrieves information to determine if we are in control of the child FindContainedWeapon match { case (Some(o), Some(tool)) => @@ -3801,16 +3882,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } else { log.warn( - s"ChildObjectState: ${player.Name} is using a different controllable agent than #${object_guid.guid}" + s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" ) } case (Some(obj), None) => log.warn( - s"ChildObjectState: ${player.Name} can not find any controllable agent, let alone #${object_guid.guid}" + s"ChildObjectState: ${player.Name} can not find any controllable agent, let alone entity ${object_guid.guid}" ) case (None, _) => ; //TODO status condition of "playing getting out of vehicle to allow for late packets without warning - //log.warn(s"ChildObjectState: player ${player.Name} not related to anything with a controllable agent") } if (player.death_by == -1) { KickedByAdministration() @@ -3829,7 +3909,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con is_decelerating, is_cloaked ) => - //log.info(s"$msg") GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle @@ -3837,7 +3916,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con turnCounterFunc(player.GUID) val seat = obj.Seats(0) player.Position = pos //convenient - if (seat.ControlledWeapon.isEmpty) { + if (obj.WeaponControlledFromSeat(0).isEmpty) { player.Orientation = Vector3.z(ang.z) //convenient } obj.Position = pos @@ -3849,12 +3928,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.Velocity = Some(Vector3.Zero) } if (obj.Definition.CanFly) { - obj.Flying = flying.nonEmpty //usually Some(7) + obj.Flying = flying //usually Some(7) } obj.Cloaked = obj.Definition.CanCloak && is_cloaked } else { obj.Velocity = None - obj.Flying = false + obj.Flying = None } continent.VehicleEvents ! VehicleServiceMessage( continent.id, @@ -3865,7 +3944,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.Position, ang, obj.Velocity, - if (obj.Flying) { + if (obj.isFlying) { flying } else { None @@ -3884,7 +3963,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle case (_, Some(index)) => log.error( - s"VehicleState: player should not be dispatching this kind of packet from vehicle#$vehicle_guid when not the driver ($index)" + s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" ) case _ => ; } @@ -3893,10 +3972,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ VehicleSubStateMessage(vehicle_guid, player_guid, vehicle_pos, vehicle_ang, vel, unk1, unk2) => - //log.info(s"VehicleSubState: $vehicle_guid, ${player.Name}_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 @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) => - //log.trace(s"ProjectileState: $msg") val index = projectile_guid.guid - Projectile.baseUID projectiles(index) match { case Some(projectile) if projectile.HasGUID => @@ -3924,16 +4002,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ ReleaseAvatarRequestMessage() => - log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.id} has released") + log.info(s"${player.Name} on ${continent.id} has released") reviveTimer.cancel() GoToDeploymentMap() HandleReleaseAvatar(player, continent) case msg @ SpawnRequestMessage(u1, spawnGroup, u3, u4, zoneNumber) => - log.info(s"SpawnRequestMessage: $msg") + log.info(s"${player.Name} on ${continent.id} wants to respawn in zone #$zoneNumber") if (deadState != DeadState.RespawnTime) { deadState = DeadState.RespawnTime - cluster ! InterstellarClusterService.GetNearbySpawnPoint( + cluster ! ICS.GetNearbySpawnPoint( spawnGroup match { case SpawnGroup.Sanctuary => Zones.sanctuaryZoneNumber(player.Faction) @@ -3945,37 +4023,37 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con context.self ) } else { - log.warn("SpawnRequestMessage: request consumed; already respawning ...") + log.warn(s"SpawnRequestMessage: request consumed because ${player.Name} is already respawning ...") } - case msg @ SetChatFilterMessage(send_channel, origin, whitelist) => - //log.info("SetChatFilters: " + msg) + case _ : SetChatFilterMessage => //msg @ SetChatFilterMessage(send_channel, origin, whitelist) => ; - case msg: ChatMsg => + case msg : ChatMsg => chatActor ! ChatActor.Message(msg) - case msg @ VoiceHostRequest(unk, PlanetSideGUID(player_guid), data) => - log.info("Player " + player_guid + " requested in-game voice chat.") + case _ : VoiceHostRequest => + log.trace(s"VoiceHostRequest: ${player.Name} requested in-game voice chat.") sendResponse(VoiceHostKill()) + sendResponse( + ChatMsg(ChatMessageType.CMT_OPEN, false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None) + ) - case msg @ VoiceHostInfo(player_guid, data) => + case _ : VoiceHostInfo => sendResponse(VoiceHostKill()) case msg @ ChangeAmmoMessage(item_guid, unk1) => - log.info("ChangeAmmo: " + msg) FindContainedEquipment match { case (Some(_), Some(obj: ConstructionItem)) => PerformConstructionItemAmmoChange(obj, obj.AmmoTypeIndex) - case (Some(obj), Some(tool: Tool)) => + case (Some(obj: PlanetSideServerObject), Some(tool: Tool)) => PerformToolAmmoChange(tool, obj) case (_, Some(obj)) => - log.error(s"ChangeAmmo: the object ${obj.Definition.Name} is not a valid type") + log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition") case (_, None) => - log.error(s"ChangeAmmo: can not find $item_guid") + log.warn(s"ChangeAmmo: can not find $item_guid") } case msg @ ChangeFireModeMessage(item_guid, fire_mode) => - log.info("ChangeFireMode: " + msg) FindEquipment match { case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => val originalModeIndex = obj.FireModeIndex @@ -3991,7 +4069,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.FireModeIndex = originalModeIndex sendResponse(ChangeFireModeMessage(tool_guid, originalModeIndex)) //reinforcement } else { - log.info(s"ChangeFireMode: changing $tool_guid to fire mode $modeIndex") + log.info(s"${player.Name} is changing his ${obj.Definition.Name} to fire mode #$modeIndex") sendResponse(ChangeFireModeMessage(tool_guid, modeIndex)) continent.AvatarEvents ! AvatarServiceMessage( continent.id, @@ -3999,13 +4077,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } case Some(_) => - log.error(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes") + log.warn(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes") case None => - log.error(s"ChangeFireMode: can not find $item_guid") + log.warn(s"ChangeFireMode: can not find $item_guid") } case msg @ ChangeFireStateMessage_Start(item_guid) => - log.trace("ChangeFireState_Start: " + msg) if (shooting.isEmpty) { FindEquipment match { case Some(tool: Tool) => @@ -4033,7 +4110,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } else { log.warn( - s"ChangeFireState_Start: ${tool.Definition.Name} magazine is empty before trying to shoot bullet" + s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" ) EmptyMagazine(item_guid, tool) } @@ -4046,12 +4123,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con AvatarAction.ChangeFireState_Start(player.GUID, item_guid) ) case None => - log.error(s"ChangeFireState_Start: can not find $item_guid") + log.warn(s"ChangeFireState_Start: can not find $item_guid") } } case msg @ ChangeFireStateMessage_Stop(item_guid) => - log.trace("ChangeFireState_Stop: " + msg) prefire = None shootingStop = System.currentTimeMillis() val weapon: Option[Equipment] = if (shooting.contains(item_guid)) { @@ -4088,7 +4164,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) Some(tool) case _ => - log.warn(s"ChangeFireState_Stop: received an unexpected message about $item_guid") + log.warn(s"ChangeFireState_Stop: ${player.Name} never started firing item ${item_guid.guid} in the first place?") None } } @@ -4110,24 +4186,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) continent.GUID(trigger.Companion) match { case Some(boomer: BoomerDeployable) => - boomer.Destroyed = true - continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.Detonate(boomer.GUID, boomer)) - Deployables.AnnounceDestroyDeployable(boomer, Some(500 milliseconds)) + boomer.Actor ! CommonMessages.Use(player, Some(trigger)) case Some(_) | None => ; } - FindEquipmentToDelete(item_guid, trigger) - trigger.Companion = None case _ => ; } progressBarUpdate.cancel() progressBarValue = None case msg @ EmoteMsg(avatar_guid, emote) => - log.info("Emote: " + msg) sendResponse(EmoteMsg(avatar_guid, emote)) case msg @ DropItemMessage(item_guid) => - log.info(s"DropItem: $msg") ValidObject(item_guid) match { case Some(anItem: Equipment) => player.FreeHand.Equipment match { @@ -4148,11 +4218,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.warn(s"DropItem: ${player.Name} wanted to drop a $obj, but that isn't possible") case None => sendResponse(ObjectDeleteMessage(item_guid, 0)) //this is fine; item doesn't exist to the server anyway - log.warn(s"DropItem: ${player.Name} wanted to drop an item ($item_guid), but it was nowhere to be found") + log.warn(s"DropItem: ${player.Name} wanted to drop an item ${item_guid.guid}, but it was nowhere to be found") } case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) => - log.info(s"PickupItem: $msg") ValidObject(item_guid) match { case Some(item: Equipment) => player.Fit(item) match { @@ -4164,30 +4233,25 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case _ => log.warn( - s"PickupItem: ${player.Name} requested an item that doesn't exist in this zone; assume client-side garbage data" + s"PickupItem: ${player.Name} requested an item ${item_guid.guid} that doesn't seem to exist" ) - sendResponse(ObjectDeleteMessage(item_guid, 0)) } case msg @ ReloadMessage(item_guid, ammo_clip, unk1) => - log.info("Reload: " + msg) FindContainedWeapon match { - case (Some(obj), Some(tool: Tool)) => + case (Some(obj: PlanetSideServerObject with Container), Some(tool: Tool)) => val currentMagazine: Int = tool.Magazine val magazineSize: Int = tool.MaxMagazine val reloadValue: Int = magazineSize - currentMagazine if (magazineSize > 0 && reloadValue > 0) { FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { - case Nil => - log.warn(s"ReloadMessage: no ammunition could be found for $item_guid") + case Nil => ; case x :: xs => val (deleteFunc, modifyFunc): (Equipment => Future[Any], (AmmoBox, Int) => Unit) = obj match { case veh: Vehicle => (RemoveOldEquipmentFromInventory(veh), ModifyAmmunitionInVehicle(veh)) - case o: PlanetSideServerObject with Container => - (RemoveOldEquipmentFromInventory(o), ModifyAmmunition(o)) case _ => - throw new Exception("ReloadMessage: should be a server object, not a regular game object") + (RemoveOldEquipmentFromInventory(obj), ModifyAmmunition(obj)) } xs.foreach { item => deleteFunc(item.obj) } val box = x.obj.asInstanceOf[AmmoBox] @@ -4197,36 +4261,36 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum } val sumReloadValue: Int = box.Capacity + tailReloadValue - val actualReloadValue = (if (sumReloadValue <= reloadValue) { - deleteFunc(box) - sumReloadValue - } else { - modifyFunc(box, reloadValue - tailReloadValue) - reloadValue - }) + currentMagazine - log.info(s"ReloadMessage: success, $tool <- $actualReloadValue ${tool.AmmoType}") - tool.Magazine = actualReloadValue - sendResponse(ReloadMessage(item_guid, actualReloadValue, unk1)) + val actualReloadValue = if (sumReloadValue <= reloadValue) { + deleteFunc(box) + sumReloadValue + } else { + modifyFunc(box, reloadValue - tailReloadValue) + reloadValue + } + val finalReloadValue = actualReloadValue + currentMagazine + log.info(s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}") + tool.Magazine = finalReloadValue + sendResponse(ReloadMessage(item_guid, finalReloadValue, unk1)) continent.AvatarEvents ! AvatarServiceMessage( continent.id, AvatarAction.Reload(player.GUID, item_guid) ) } } else { - log.warn(s"ReloadMessage: item $item_guid can not reload (full=$magazineSize, want=$reloadValue)") + log.warn(s"ReloadMessage: the ${tool.Definition.Name} under ${player.Name}'s control can not reload (full=$magazineSize, want=$reloadValue)") } case (_, Some(_)) => - log.error(s"ReloadMessage: the object that was found for $item_guid was not a Tool") + log.warn(s"ReloadMessage: the object that was found for $item_guid was not a Tool") case (_, None) => - log.error(s"ReloadMessage: can not find $item_guid") + log.warn(s"ReloadMessage: can not find $item_guid") } case msg @ ObjectHeldMessage(avatar_guid, held_holsters, unk1) => - log.debug(s"ObjectHeld: $msg") val before = player.DrawnSlot if (before != held_holsters) { if (player.ExoSuit == ExoSuitType.MAX && held_holsters != 0) { - log.info(s"ObjectHeld: ${player.Name} is denied changing hands to $held_holsters as a MAX") + log.warn(s"ObjectHeld: ${player.Name} is denied changing hands to $held_holsters as a MAX") player.DrawnSlot = 0 sendResponse(ObjectHeldMessage(avatar_guid, 0, true)) } else if ((player.DrawnSlot = held_holsters) != before) { @@ -4234,12 +4298,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.Continent, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot) ) - // Ignore non-equipment holsters //todo: check current suit holster slots? if (held_holsters >= 0 && held_holsters < 5) { player.Holsters()(held_holsters).Equipment match { case Some(unholsteredItem: Equipment) => + log.info(s"${player.Name} has drawn a $unholsteredItem from its holster") if (unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) { // Player has unholstered a REK - we need to set an atttribute on the REK itself to change the beam/icon colour to the correct one for the player's hack level continent.AvatarEvents ! AvatarServiceMessage( @@ -4267,12 +4331,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con avatarActor ! AvatarActor.SuspendStaminaRegeneration(2.5 seconds) case msg @ ZipLineMessage(player_guid, forwards, action, path_id, pos) => - log.info("ZipLineMessage: " + msg) val (isTeleporter: Boolean, path: Option[ZipLinePath]) = continent.zipLinePaths.find(x => x.PathId == path_id) match { case Some(x) => (x.IsTeleporter, Some(x)) case _ => - log.warn(s"Couldn't find zipline path ${path_id} in zone ${continent.Number} / ${continent.id}") + log.warn(s"${player.Name} couldn't find a zipline path $path_id in zone ${continent.id}") (false, None) } if (isTeleporter) { @@ -4296,7 +4359,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(ZipLineMessage(player_guid, forwards, action, 0, pos)) case _ => log.warn( - s"Tried to do something with a zipline but can't handle it. forwards: ${forwards} action: ${action} path_id: ${path_id} zone: ${continent.Number} / ${continent.id}" + s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action path_id: $path_id zone: ${continent.Number} / ${continent.id}" ) } } @@ -4307,20 +4370,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(vehicle: Vehicle) => /* line 1a: player is admin (and overrules other access requirements) */ /* line 1b: vehicle and player (as the owner) acknowledge each other */ - /* line 1c: vehicle is the same faction as player and either the owner is absent or the vehicle is destroyed */ + /* line 1c: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */ /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */ if ( (session.account.gm || (player.avatar.vehicle.contains(object_guid) && vehicle.Owner.contains(player.GUID)) || - (player.Faction == vehicle.Faction && ((vehicle.Owner.isEmpty || continent - .GUID(vehicle.Owner.get) - .isEmpty) || vehicle.Destroyed))) && + (player.Faction == vehicle.Faction && + (vehicle.Definition.CanBeOwned.nonEmpty && + (vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Destroyed))) && (vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied)) ) { vehicle.Actor ! Vehicle.Deconstruct() - log.info(s"RequestDestroy: vehicle $vehicle") + //log.info(s"RequestDestroy: vehicle $vehicle") } else { - log.info(s"RequestDestroy: must own vehicle in order to deconstruct it") + log.warn(s"RequestDestroy: ${player.Name} must own vehicle in order to deconstruct it") } case Some(obj: BoomerTrigger) => @@ -4333,7 +4396,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) //continent.Deployables ! Zone.Deployable.Dismiss(boomer) case Some(thing) => - log.info(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing") + log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing") case None => ; } } @@ -4405,10 +4468,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ ObjectDeleteMessage(object_guid, unk1) => sendResponse(ObjectDeleteMessage(object_guid, 0)) - log.info("ObjectDelete: " + msg) case msg @ MoveItemMessage(item_guid, source_guid, destination_guid, dest, _) => - log.info(s"MoveItem: $msg") (continent.GUID(source_guid), continent.GUID(destination_guid), ValidObject(item_guid)) match { case ( Some(source: PlanetSideServerObject with Container), @@ -4417,21 +4478,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) => ContainableMoveItem(player.Name, source, destination, item, dest) case (None, _, _) => - log.error(s"MoveItem: wanted to move $item_guid from $source_guid, but could not find source object") + log.error(s"MoveItem: ${player.Name} wanted to move $item_guid from $source_guid, but could not find source object") case (_, None, _) => log.error( - s"MoveItem: wanted to move $item_guid to $destination_guid, but could not find destination object" + s"MoveItem: ${player.Name} wanted to move $item_guid to $destination_guid, but could not find destination object" ) case (_, _, None) => - log.error(s"MoveItem: wanted to move $item_guid, but could not find it") + log.error(s"MoveItem: ${player.Name} wanted to move $item_guid, but could not find it") case _ => log.error( - s"MoveItem: wanted to move $item_guid from $source_guid to $destination_guid, but multiple problems were encountered" + s"MoveItem: ${player.Name} wanted to move $item_guid from $source_guid to $destination_guid, but multiple problems were encountered" ) } case msg @ LootItemMessage(item_guid, target_guid) => - log.info(s"LootItem: $msg") (ValidObject(item_guid), continent.GUID(target_guid)) match { case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) => //figure out the source @@ -4454,20 +4514,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case (Some((source, Some(_))), Some(dest)) => ContainableMoveItem(player.Name, source, destination, item, dest) case (None, _) => - log.error(s"LootItem: can not find where $item is put currently") + log.error(s"LootItem: ${player.Name} can not find where $item is put currently") case (_, None) => - log.error(s"LootItem: can not find somwhere to put $item in $destination") + log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination") case _ => log.error( - s"LootItem: wanted to move $item_guid to $target_guid, but multiple problems were encountered" + s"LootItem: ${player.Name}wanted to move $item_guid to $target_guid, but multiple problems were encountered" ) } case (Some(obj), _) => - log.warn(s"LootItem: item $obj is (probably) not lootable") + log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}") case (None, _) => - log.warn(s"LootItem: can not find $item_guid") + log.error(s"LootItem: ${player.Name} can not find $item_guid") case (_, None) => - log.warn(s"LootItem: can not find where to put $item_guid") + log.error(s"LootItem: ${player.Name} can not find where to put $item_guid") } case msg @ AvatarImplantMessage(player_guid, action, slot, status) => @@ -4481,7 +4541,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) } case Some(implant) if !implant.initialized => () - case _ => log.error(s"AvatarImplantMessage for unknown implant ${msg}") + case _ => log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in $slot") } } @@ -4498,7 +4558,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con unk8, itemType ) => - //log.info("UseItem: " + msg) // TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok) // TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg) val equipment = player.Slot(player.DrawnSlot).Equipment match { @@ -4524,7 +4583,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con CancelZoningProcessWithDescriptiveReason("cancel_use") if (obj.isBackpack) { if (equipment.isEmpty) { - log.info(s"UseItem: ${player.Name} looting the corpse of $obj") + log.info(s"${player.Name} is looting the corpse of ${obj.Name}") sendResponse( UseItemMessage( avatar_guid, @@ -4632,12 +4691,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con true } } else { - log.warn(s"UseItem: $kit behavior not supported") + log.warn(s"UseItem: Your $kit behavior is not supported, ${player.Name}") false } case None => - log.error(s"UseItem: anticipated a $kit, but can't find it") + log.error(s"UseItem: Anticipated a $kit for ${player.Name}, but can't find it") false } if (kitIsUsed) { @@ -4666,9 +4725,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case Some(item) => - log.warn(s"UseItem: looking for Kit to use, but found $item instead") + log.warn(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead") case None => - log.warn(s"UseItem: anticipated a Kit $item_used_guid, but can't find it") + log.error(s"UseItem: anticipated a Kit $item_used_guid for ${player.Name}, but can't find it") } } else if (itemType == ObjectClass.avatar && unk3) { equipment match { @@ -4686,8 +4745,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(item) => CancelZoningProcessWithDescriptiveReason("cancel_use") locker.Actor ! CommonMessages.Use(player, Some(item)) - case None if locker.Faction == player.Faction || !locker.HackedBy.isEmpty => - log.trace(s"UseItem: ${player.Name} accessing a locker") + case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty => + log.info(s"${player.Name} is accessing a locker") CancelZoningProcessWithDescriptiveReason("cancel_use") val playerLocker = player.avatar.locker sendResponse( @@ -4764,6 +4823,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner .contains(player.GUID)) ) { + log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk") CancelZoningProcessWithDescriptiveReason("cancel_use") obj.AccessingTrunk = player.GUID AccessContainer(obj) @@ -4787,7 +4847,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case Some(terminal: Terminal) => - log.info(s"$msg") equipment match { case Some(item) => CancelZoningProcessWithDescriptiveReason("cancel_use") @@ -4808,6 +4867,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) { FindLocalVehicle match { case Some(vehicle) => + log.info(s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}") sendResponse( UseItemMessage( avatar_guid, @@ -4839,16 +4899,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) case None => - log.error("UseItem: expected seated vehicle, but found none") + log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none") } } else if (tdef == GlobalDefinitions.teleportpad_terminal) { //explicit request + log.info(s"${player.Name} is purchasing a router telepad") CancelZoningProcessWithDescriptiveReason("cancel_use") terminal.Actor ! Terminal.Request( player, ItemTransactionMessage(object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) ) } else { + log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}") CancelZoningProcessWithDescriptiveReason("cancel_use") sendResponse( UseItemMessage( @@ -4877,6 +4939,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.Actor ! CommonMessages.Use(player, Some(item)) case None if player.Faction == obj.Faction => //deconstruction + log.info(s"${player.Name} is deconstructing at the ${obj.Owner.Definition.Name}'s spawns") CancelZoningProcessWithDescriptiveReason("cancel_use") PlayerActionsToCancel() CancelAllProximityUnits() @@ -4976,38 +5039,39 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(obj) => CancelZoningProcessWithDescriptiveReason("cancel_use") - log.warn(s"UseItem: don't know how to handle $obj") + log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") case None => - log.error(s"UseItem: can not find object $object_guid") + log.error(s"UseItem: ${player.Name} can not find object $object_guid") } case msg @ ProximityTerminalUseMessage(player_guid, object_guid, _) => - log.trace(s"ProximityTerminalUse: $msg") continent.GUID(object_guid) match { case Some(obj: Terminal with ProximityUnit) => HandleProximityTerminalUse(obj) case Some(obj) => ; - log.warn(s"ProximityTerminalUse: object does not have proximity effects - $obj") + log.warn(s"ProximityTerminalUse: $obj does not have proximity effects for ${player.Name}") case None => - log.warn(s"ProximityTerminalUse: no object with guid $object_guid found") + log.error(s"ProximityTerminalUse: ${player.Name} can not find an oject with guid $object_guid") } case msg @ UnuseItemMessage(player_guid, object_guid) => - log.info(s"UnuseItem: $msg") ValidObject(object_guid) match { case Some(obj: Player) => UnaccessContainer(obj) TryDisposeOfLootedCorpse(obj) case Some(obj: Container) => - UnaccessContainer(obj) + // Make sure we don't unload the contents of the vehicle the player is seated in + // An example scenario of this would be closing the trunk contents when rearming at a landing pad + if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != obj.GUID) { + UnaccessContainer(obj) + } case _ => ; } case msg @ DeployObjectMessage(guid, unk1, pos, orient, unk2) => - log.info(s"DeployObject: $msg") //the hand with the construction item is no longer drawn //TODO consider player.Slot(player.LastDrawnSlot) (player.Holsters().find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid) match { @@ -5023,7 +5087,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case turret => turret } - log.info(s"DeployObject: Constructing a ${ammoType}") + log.info(s"${player.Name} is constructing a $ammoType deployable") CancelZoningProcessWithDescriptiveReason("cancel_use") val dObj: PlanetSideGameObject with Deployable = Deployables.Make(ammoType)() dObj.Position = pos @@ -5039,31 +5103,28 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.tasks ! CallBackForTask(tasking, continent.Deployables, Zone.Deployable.Build(dObj, obj)) case Some(obj) => - log.warn(s"DeployObject: $obj is something?") + log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") case None => - log.warn("DeployObject: nothing?") + log.error(s"DeployObject: nothing, ${player.Name}? It's not a construction tool!") } case msg @ GenericObjectActionMessage(object_guid, code) => - //log.info(s"$msg") ValidObject(object_guid) match { case Some(tool: Tool) => if (tool.Definition == GlobalDefinitions.maelstrom && code == 35) { //maelstrom primary fire mode effect (no target) HandleWeaponFireAccountability(object_guid, PlanetSideGUID(Projectile.baseUID)) } - case _ => - log.info(s"$msg") + case _ => ; } case msg @ GenericObjectStateMsg(object_guid, unk1) => - log.info("GenericObjectState: " + msg) + log.debug("GenericObjectState: " + msg) case msg @ GenericActionMessage(action) => - log.info(s"GenericAction: $msg") if (player == null) { if (action == 29) { - log.info("AFK state reported during login") + log.debug("GenericObjectState: AFK state reported during login") } } else { val (toolOpt, definition) = player.Slot(0).Equipment match { @@ -5083,7 +5144,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con DropSpecialSlotItem() } if (action == 15) { //max deployment - log.info(s"GenericObject: $player is anchored") + log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground") player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored continent.AvatarEvents ! AvatarServiceMessage( continent.id, @@ -5101,10 +5162,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con tool.ToFireMode = convertFireModeIndex sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex)) case _ => - log.warn(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}") + log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}") } } else if (action == 16) { //max deployment - log.info(s"GenericObject: $player has released the anchors") + log.info(s"${player.Name} has released the anchors") player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal continent.AvatarEvents ! AvatarServiceMessage( continent.id, @@ -5122,23 +5183,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con tool.ToFireMode = convertFireModeIndex sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex)) case _ => - log.warn(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}") + log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}") } } else if (action == 20) { if (player.ExoSuit == ExoSuitType.MAX) { ToggleMaxSpecialState(enable = true) } else { - log.warn("Got GenericActionMessage 20 but can't handle it") + log.warn(s"GenericActionMessage: ${player.Name} can't handle action code 20") } } else if (action == 21) { if (player.ExoSuit == ExoSuitType.MAX) { player.Faction match { case PlanetSideEmpire.NC => ToggleMaxSpecialState(enable = false) - case _ => log.warn(s"Player ${player.Name} tried to cancel an uncancellable MAX special ability") + case _ => log.warn(s"GenericActionMessage: ${player.Name} tried to cancel an uncancellable MAX special ability") } } else { - log.warn("Got GenericActionMessage 21 but can't handle it") + log.warn(s"GenericActionMessage: ${player.Name} can't handle action code 21") } } else if (action == 36) { //Looking For Squad ON if (squadUI.nonEmpty) { @@ -5164,46 +5225,46 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } else if (avatar.lookingForSquad) { avatarActor ! AvatarActor.SetLookingForSquad(false) } + } else { + log.debug(s"$msg") } } case msg @ ItemTransactionMessage(terminal_guid, transaction_type, _, _, _, _) => - log.info("ItemTransaction: " + msg) continent.GUID(terminal_guid) match { case Some(term: Terminal) => - log.info(s"ItemTransaction: ${term.Definition.Name} found") if (lastTerminalOrderFulfillment) { + log.trace(s"ItemTransactionMessage: ${player.Name} is submitting an order") lastTerminalOrderFulfillment = false CancelZoningProcessWithDescriptiveReason("cancel_use") term.Actor ! Terminal.Request(player, msg) } case Some(obj: PlanetSideGameObject) => - log.error(s"ItemTransaction: $obj is not a terminal") + log.error(s"ItemTransaction: $obj is not a terminal, ${player.Name}") case _ => - log.error(s"ItemTransaction: $terminal_guid does not exist") + log.error(s"ItemTransaction: $terminal_guid does not exist, ${player.Name}") } case msg @ FavoritesRequest(player_guid, loadoutType, action, line, label) => CancelZoningProcessWithDescriptiveReason("cancel_use") - log.info(s"FavoritesRequest: $msg") + log.info(s"${player.Name} wishes to load a saved favorite loadout") action match { case FavoritesAction.Save => avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) case FavoritesAction.Delete => avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line) - case FavoritesAction.Unknown => log.warn("FavoritesRequest: unknown favorites action") + case FavoritesAction.Unknown => log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action") } - case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) => - log.info("WeaponDelayFire: " + msg) + case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) => ; case msg @ WeaponDryFireMessage(weapon_guid) => - log.debug("WeaponDryFireMessage: " + msg) - FindWeapon match { - case Some(tool: Tool) => + FindWeapon.orElse { continent.GUID(weapon_guid) } match { + case Some(_: Equipment) => continent.AvatarEvents ! AvatarServiceMessage( continent.id, AvatarAction.WeaponDryFire(player.GUID, weapon_guid) ) - case _ => ; + case _ => + log.warn(s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist") } case msg @ WeaponFireMessage( @@ -5219,14 +5280,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con unk6, unk7 ) => - log.debug(s"WeaponFire: $msg") HandleWeaponFire(weapon_guid, projectile_guid, shot_origin) - case msg @ WeaponLazeTargetPositionMessage(weapon, pos1, pos2) => - log.info("Lazing position: " + pos2.toString) + case msg @ WeaponLazeTargetPositionMessage(_, _, pos2) => + log.debug(s"${player.Name} is lazing the position $pos2; to what ends?") case msg @ ObjectDetectedMessage(guid1, guid2, unk, targets) => - //log.info(s"Detection: $msg") FindWeapon match { case Some(weapon) if weapon.Projectile.AutoLock => //projectile with auto-lock instigates a warning on the target @@ -5249,7 +5308,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con unk3, unk4 ) => - log.info(s"Hit: $msg") + log.trace(s"${player.Name} lands a hit - $msg") //find defined projectile FindProjectileEntry(projectile_guid) match { case Some(projectile) => @@ -5324,7 +5383,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con unk4, targets ) => - log.info(s"Splash: $msg") + log.trace(s"${player.Name} splashes some targets - $msg") FindProjectileEntry(projectile_guid) match { case Some(projectile) => val profile = projectile.profile @@ -5341,8 +5400,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target) ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match { - case Some(projectile) => - HandleDealingDamage(target, projectile) + case Some(_projectile) => + HandleDealingDamage(target, _projectile) case None => ; } case _ => ; @@ -5353,13 +5412,25 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match { - case Some(projectile) => - HandleDealingDamage(target, projectile) + case Some(_projectile) => + HandleDealingDamage(target, _projectile) case None => ; } case _ => ; } }) + if ( + projectile.profile.HasJammedEffectDuration || + projectile.profile.JammerProjectile || + projectile.profile.SympatheticExplosion + ) { + Zone.causeSpecialEmp( + continent, + player, + explosion_pos, + GlobalDefinitions.special_emp.innateDamage.get + ) + } if (profile.ExistsOnRemoteClients && projectile.HasGUID) { //cleanup val localIndex = projectile_guid.guid - Projectile.baseUID @@ -5373,7 +5444,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ LashMessage(seq_time, killer_guid, victim_guid, projectile_guid, hit_pos, unk1) => - log.info(s"Lash: $msg") + log.trace(s"${player.Name} lashes some targets - $msg") ValidObject(victim_guid) match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target) @@ -5396,63 +5467,58 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con unk1, unk2 ) => - log.info(s"WarpgateRequest: $msg") CancelZoningProcessWithDescriptiveReason("cancel_use") if (deadState != DeadState.RespawnTime) { continent.Buildings.values.find(building => building.GUID == building_guid) match { - case Some(wg: WarpGate) if (wg.Active && (GetKnownVehicleAndSeat() match { + case Some(wg: WarpGate) if wg.Active && (GetKnownVehicleAndSeat() match { case (Some(vehicle), _) => wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition) case _ => true - })) => + }) => deadState = DeadState.RespawnTime - cluster ! InterstellarClusterService.GetSpawnPoint( + cluster ! ICS.GetSpawnPoint( destinationZoneGuid.guid, player, destinationBuildingGuid, context.self ) + log.info(s"${player.Name} is trying to use a warp gate") - case Some(wg: WarpGate) if (!wg.Active) => - log.info(s"WarpgateRequest: inactive WarpGate") + case Some(wg: WarpGate) if !wg.Active => + log.warn(s"WarpgateRequest: ${player.Name} is knocking on an inactive warp gate") case _ => deadState = DeadState.RespawnTime RequestSanctuaryZoneSpawn(player, continent.Number) } } else { - log.warn("WarpgateRequest: request consumed; already respawning ...") + log.debug(s"WarpgateRequest: your request was already consumed, ${player.Name}; already working on it ...") } case msg @ MountVehicleMsg(player_guid, mountable_guid, entry_point) => - log.info("MountVehicleMsg: " + msg) ValidObject(mountable_guid) match { case Some(obj: Mountable) => - obj.GetSeatFromMountPoint(entry_point) match { - case Some(seat_num) => - obj.Actor ! Mountable.TryMount(player, seat_num) - case None => - log.warn( - s"MountVehicleMsg: attempted to board mountable $mountable_guid's seat $entry_point, but no seat exists there" - ) - } + obj.Actor ! Mountable.TryMount(player, entry_point) + case None | Some(_) => - log.warn(s"MountVehicleMsg: not a mountable thing") + log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}") } case msg @ DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) => //TODO optimize this later - log.info(s"DismountVehicleMsg: $msg") //common warning for this section - def dismountWarning(msg: String): Unit = { - log.warn(s"$msg; some vehicle might not know that a player is no longer sitting in it") + def dismountWarning(note: String): Unit = { + log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it") } if (player.GUID == player_guid) { - //normally disembarking from a seat + //normally disembarking from a mount (interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { case out @ Some(obj: Vehicle) => - if (obj.MountedIn.isEmpty) out else None + continent.GUID(obj.MountedIn) match { + case Some(_: Vehicle) => None //cargo vehicle + case _ => out //arrangement "may" be permissible + } case out @ Some(_: Mountable) => out case _ => @@ -5465,7 +5531,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.PassengerInSeat(player) match { case Some(0) if controlled.nonEmpty => log.warn( - s"DismountVehicleMsg: can not dismount from vehicle as driver while server has asserted control; please wait ..." + s"DismountVehicleMsg: ${player.Name} can not dismount from vehicle as driver while server has asserted control; please wait ..." ) case Some(seat_num) => obj.Actor ! Mountable.TryDismount(player, seat_num) @@ -5479,7 +5545,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. //todo: kick cargo passengers out. To be added after PR #216 is merged obj match { - case v: Vehicle if bailType == BailType.Bailed && seat_num == 0 && v.Flying => + case v: Vehicle + if bailType == BailType.Bailed && + v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) && + v.isFlying => v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction case _ => ; } @@ -5493,7 +5562,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con dismountWarning(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}") } } else { - //kicking someone else out of a seat; need to own that seat/mountable + //kicking someone else out of a mount; need to own that mount/mountable player.avatar.vehicle match { case Some(obj_guid) => ((ValidObject(obj_guid), ValidObject(player_guid)) match { @@ -5516,7 +5585,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case (None, _) => ; log.warn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle") case (_, None) => ; - log.warn(s"DismountVehicleMsg: player $player_guid could not be found to kick") + log.warn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}") case _ => log.warn(s"DismountVehicleMsg: object is either not a Mountable or not a Player") } @@ -5526,29 +5595,32 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ DeployRequestMessage(player_guid, vehicle_guid, deploy_state, unk2, unk3, pos) => - log.info(s"DeployRequest: $msg") - if (player.avatar.vehicle.contains(vehicle_guid) && player.avatar.vehicle == player.VehicleSeated) { - continent.GUID(vehicle_guid) match { - case Some(obj: Vehicle) => - obj.Actor ! Deployment.TryDeploymentChange(deploy_state) + if (player.avatar.vehicle.contains(vehicle_guid)) { + if (player.avatar.vehicle == player.VehicleSeated) { + continent.GUID(vehicle_guid) match { + case Some(obj: Vehicle) => + log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name}") + obj.Actor ! Deployment.TryDeploymentChange(deploy_state) - case _ => - log.error(s"DeployRequest: can not find $vehicle_guid in scope") - avatarActor ! AvatarActor.SetVehicle(None) + case _ => + log.error(s"DeployRequest: ${player.Name} can not find vehicle $vehicle_guid") + avatarActor ! AvatarActor.SetVehicle(None) + } + } else { + log.warn(s"${player.Name} must be mounted to request a deployment change") } } else { log.warn(s"DeployRequest: ${player.Name} does not own the deploying $vehicle_guid object") } case msg @ AvatarGrenadeStateMessage(player_guid, state) => - log.info("AvatarGrenadeStateMessage: " + msg) + //TODO I thought I had this working? + log.info(s"${player.Name} has $state ${player.Sex.possessive} grenade") case msg @ SquadDefinitionActionMessage(u1, u2, action) => - log.info(s"SquadDefinitionAction: $msg") squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) case msg @ SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) => - log.info(s"$msg") squadService ! SquadServiceMessage( player, continent, @@ -5556,11 +5628,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) case msg @ SquadWaypointRequest(request, _, wtype, unk, info) => - log.info(s"Waypoint Request: $msg") 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.debug("Ouch! " + msg) + log.info(s"${player.Name} would be in intense and excruciating pain right now if collision worked") case msg @ BugReportMessage( version_major, @@ -5574,13 +5645,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con summary, desc ) => - log.info("BugReportMessage: " + msg) + log.warn(s"${player.Name} filed a bug report") + log.debug(s"$msg") case msg @ BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) => - log.info("BindPlayerMessage: " + msg) + //log.info("BindPlayerMessage: " + msg) case msg @ PlanetsideAttributeMessage(object_guid, attribute_type, attribute_value) => - log.info("PlanetsideAttributeMessage: " + msg) ValidObject(object_guid) match { case Some(vehicle: Vehicle) => if (player.avatar.vehicle.contains(vehicle.GUID)) { @@ -5588,56 +5659,48 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con vehicle.PermissionGroup(attribute_type, attribute_value) match { case Some(allow) => val group = AccessPermissionGroup(attribute_type - 10) - log.info(s"Vehicle attributes: vehicle ${vehicle.GUID} access permission $group changed to $allow") + log.info(s"${player.Name} changed ${vehicle.Definition.Name}'s access permission $group to $allow") continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.SeatPermissions(player.GUID, vehicle.GUID, attribute_type, attribute_value) ) //kick players who should not be seated in the vehicle due to permission changes if (allow == VehicleLockState.Locked) { //TODO only important permission atm - vehicle.Definition.MountPoints.values - .foreach(mountpoint_num => { - vehicle.Seat(mountpoint_num) match { - case Some(seat) => - seat.Occupant match { - case Some(tplayer) => - if ( - vehicle.SeatPermissionGroup(mountpoint_num).contains(group) && tplayer != player - ) { //can not kick self - seat.Occupant = None - tplayer.VehicleSeated = None - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.KickPassenger(tplayer.GUID, 4, false, object_guid) - ) - } - case None => ; // No player seated - } - case None => ; // Not a seat mounting point - } - vehicle.CargoHold(mountpoint_num) match { - case Some(cargo) => - cargo.Occupant match { - case Some(vehicle) => - if (vehicle.SeatPermissionGroup(mountpoint_num).contains(group)) { - //todo: this probably doesn't work for passengers within the cargo vehicle - // Instruct client to start bail dismount procedure - self ! DismountVehicleCargoMsg(player.GUID, vehicle.GUID, true, false, false) - } - case None => ; // No vehicle in cargo - } - case None => ; // Not a cargo mounting point - } - - }) + vehicle.Seats.foreach { case (seatIndex, seat) => + seat.occupant match { + case Some(tplayer : Player) => + if ( + vehicle.SeatPermissionGroup(seatIndex).contains(group) && tplayer != player + ) { //can not kick self + seat.unmount(tplayer) + tplayer.VehicleSeated = None + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.KickPassenger(tplayer.GUID, 4, false, object_guid) + ) + } + case _ => ; // No player seated + } + } + vehicle.CargoHolds.foreach { case (cargoIndex, hold) => + hold.occupant match { + case Some(cargo) => + if (vehicle.SeatPermissionGroup(cargoIndex).contains(group)) { + //todo: this probably doesn't work for passengers within the cargo vehicle + // Instruct client to start bail dismount procedure + self ! DismountVehicleCargoMsg(player.GUID, cargo.GUID, true, false, false) + } + case None => ; // No vehicle in cargo + } + } } case None => ; } } else { - log.warn(s"Vehicle attributes: unsupported change on vehicle $object_guid - $attribute_type") + log.warn(s"PlanetsideAttribute: vehicle attributes - unsupported change on vehicle $object_guid - $attribute_type, ${player.Name}") } } else { - log.warn(s"Vehicle attributes: ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it") + log.warn(s"PlanetsideAttribute: vehicle attributes - ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it") } // Cosmetics options @@ -5645,7 +5708,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attribute_value)) case _ => - log.warn(s"echo unknown attributes behavior") + log.warn(s"PlanetsideAttribute: echoing unknown attributes behavior $attribute_type back to ${player.Name}") sendResponse(PlanetsideAttributeMessage(object_guid, attribute_type, attribute_value)) } @@ -5659,27 +5722,25 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case _ => log.warn( - s"FacilityBenefitShieldChargeRequest: can not find vehicle ${vehicleGUID.guid} in zone ${continent.id}" + s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find vehicle ${vehicleGUID.guid} in zone ${continent.id}" ) } case None => - log.warn(s"FacilityBenefitShieldChargeRequest: player ${player.Name} is not seated in a vehicle") + log.warn(s"FacilityBenefitShieldChargeRequest: ${player.Name} is not seated in a vehicle") } case msg @ BattleplanMessage(char_id, player_name, zone_id, diagrams) => - log.info("Battleplan: " + msg) + val lament: String = s"${player.Name} has a brilliant idea that no one will ever see" + log.info(lament) + log.debug(s"Battleplan: $lament - $msg") - case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) => - log.debug("CreateShortcutMessage: " + msg) + case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) => ; - case msg @ FriendsRequest(action, friend) => - log.info("FriendsRequest: " + msg) + case msg @ FriendsRequest(action, friend) => ; - case msg @ HitHint(source_guid, player_guid) => - log.trace(s"HitHint: $msg") //HitHint is manually distributed for proper operation + case msg @ HitHint(source_guid, player_guid) => ; //HitHint is manually distributed for proper operation case msg @ TargetingImplantRequest(list) => - log.info("TargetingImplantRequest: " + msg) val targetInfo: List[TargetInfo] = list.flatMap(x => { continent.GUID(x.target_guid) match { case Some(player: Player) => @@ -5692,18 +5753,27 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con Some(TargetInfo(player.GUID, health, armor)) case _ => - log.warn(s"Target info requested for guid ${x.target_guid} but is not a player") + log.warn(s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player") None } }) sendResponse(TargetingInfoMessage(targetInfo)) + case msg @ DroppodLaunchRequestMessage(info, _) => + //log.info(s"Droppod request: $msg") + cluster ! ICS.DroppodLaunchRequest( + info.zone_number, + info.xypos, + player.Faction, + self.toTyped[ICS.DroppodLaunchExchange] + ) + case msg @ ActionCancelMessage(u1, u2, u3) => - log.info("Cancelled: " + msg) progressBarUpdate.cancel() progressBarValue = None - case default => log.error(s"Unhandled GamePacket $pkt") + case _ => + log.warn(s"Unhandled GamePacket $pkt") } /** @@ -5730,7 +5800,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } def Execute(resolver: ActorRef): Unit = { - log.info(s"Player $localPlayer is registered") + log.trace(s"Player $localPlayer is registered") resolver ! Success(this) localAnnounce ! NewPlayerLoaded(localPlayer) //alerts WorldSessionActor } @@ -5767,7 +5837,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } def Execute(resolver: ActorRef): Unit = { - log.info(s"Player $localPlayer is registered") + log.trace(s"Player $localPlayer is registered") resolver ! Success(this) localAnnounce ! PlayerLoaded(localPlayer) //alerts WorldSessionActor } @@ -5803,7 +5873,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } def Execute(resolver: ActorRef): Unit = { - log.info(s"Vehicle $localVehicle is registered") + log.trace(s"Vehicle $localVehicle is registered") resolver ! Success(this) } }, @@ -5855,7 +5925,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } def Execute(resolver: ActorRef): Unit = { - log.info(s"Vehicle $localVehicle is registered") + log.trace(s"Vehicle $localVehicle is registered") localDriver.VehicleSeated = localVehicle.GUID Vehicles.Own(localVehicle, localDriver) localAnnounce ! PlayerLoaded(localDriver) @@ -6201,7 +6271,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Common preparation for disengaging from a corpse. * Leave the corpse-specific group that was used for shared updates. * Deconstruct every object in the backpack's inventory. - * @param vehicle the vehicle + * @param tplayer the corpse */ def UnaccessCorpseContainer(tplayer: Player): Unit = { accessedContainer = None @@ -6243,7 +6313,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Along with any discovered item, a containing object such that the statement:
* `container.Find(object) = Some(slot)`
* ... will return a proper result. - * For a seat controlled weapon, the vehicle is returned. + * For a mount controlled weapon, the vehicle is returned. * For the player's hand, the player is returned. * @return a `Tuple` of the returned values; * the first value is a `Container` object; @@ -6253,7 +6323,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.VehicleSeated match { case Some(vehicle_guid) => //weapon is vehicle turret? continent.GUID(vehicle_guid) match { - case Some(vehicle: Mountable with MountedWeapons with Container) => + case Some(vehicle: Mountable with MountableWeapons with Container) => vehicle.PassengerInSeat(player) match { case Some(seat_num) => (Some(vehicle), vehicle.WeaponControlledFromSeat(seat_num)) @@ -6360,7 +6430,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param tool na * @param obj na */ - def PerformToolAmmoChange(tool: Tool, obj: PlanetSideGameObject with Container): Unit = { + def PerformToolAmmoChange(tool: Tool, obj: PlanetSideServerObject with Container): Unit = { val originalAmmoType = tool.AmmoType do { val requestedAmmoType = tool.NextAmmoType @@ -6370,23 +6440,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Nil => ; case x :: xs => val (deleteFunc, modifyFunc): (Equipment => Future[Any], (AmmoBox, Int) => Unit) = obj match { - case (veh: Vehicle) => + case veh: Vehicle => (RemoveOldEquipmentFromInventory(veh), ModifyAmmunitionInVehicle(veh)) - case o: PlanetSideServerObject with Container => - (RemoveOldEquipmentFromInventory(o), ModifyAmmunition(o)) case _ => - throw new Exception( - "PerformToolAmmoChange: (remove/modify) should be a server object, not a regular game object" - ) - } - val (stowNewFunc, stowFunc): (Equipment => TaskResolver.GiveTask, Equipment => Future[Any]) = obj match { - case o: PlanetSideServerObject with Container => - (PutNewEquipmentInInventoryOrDrop(o), PutEquipmentInInventoryOrDrop(o)) - case _ => - throw new Exception( - "PerformToolAmmoChange: (new/put) should be a server object, not a regular game object" - ) + (RemoveOldEquipmentFromInventory(obj), ModifyAmmunition(obj)) } + val (stowNewFunc, stowFunc): (Equipment => TaskResolver.GiveTask, Equipment => Future[Any]) = + (PutNewEquipmentInInventoryOrDrop(obj), PutEquipmentInInventoryOrDrop(obj)) + xs.foreach(item => { obj.Inventory -= x.start deleteFunc(item.obj) @@ -6396,7 +6457,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val box = x.obj.asInstanceOf[AmmoBox] val originalBoxCapacity = box.Capacity val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) } + 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)) @@ -6426,28 +6487,28 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) //handle inventory contents - box.Capacity = (if (sumReloadValue <= fullMagazine) { + box.Capacity = if (sumReloadValue <= fullMagazine) { sumReloadValue } else { val splitReloadAmmo: Int = sumReloadValue - fullMagazine - log.info( - s"ChangeAmmo: taking ${originalBoxCapacity - splitReloadAmmo} from a box of ${originalBoxCapacity} $requestedAmmoType" + log.trace( + s"PerformToolAmmoChange: ${player.Name} takes ${originalBoxCapacity - splitReloadAmmo} from a box of $originalBoxCapacity $requestedAmmoType ammo" ) val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) continent.tasks ! stowNewFunc(boxForInventory) fullMagazine - }) + } sendResponse( InventoryStateMessage(box.GUID, tool.GUID, box.Capacity) ) //should work for both players and vehicles - log.info(s"ChangeAmmo: loading ${box.Capacity} $requestedAmmoType into ${tool.GUID} @ $ammoSlotIndex") + log.info(s"${player.Name} loads ${box.Capacity} $requestedAmmoType into the ${tool.Definition.Name}") if (previousBox.Capacity > 0) { //divide capacity across other existing and not full boxes of that ammo type var capacity = previousBox.Capacity val iter = obj.Inventory.Items .filter(entry => { entry.obj match { - case (item: AmmoBox) => + case item: AmmoBox => item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity case _ => false @@ -6460,7 +6521,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val entry = iter.next() val item: AmmoBox = entry.obj.asInstanceOf[AmmoBox] val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) - log.info(s"ChangeAmmo: putting $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") + log.info(s"${player.Name} put $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") capacity -= ammoAllocated modifyFunc(item, -ammoAllocated) } @@ -6477,9 +6538,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } AmmoBox.Split(previousBox) match { case Nil | List(_) => ; //done (the former case is technically not possible) - case _ :: xs => + case _ :: toUpdate => modifyFunc(previousBox, 0) //update to changed capacity value - xs.foreach(box => { continent.tasks ! stowNewFunc(box) }) + toUpdate.foreach(box => { continent.tasks ! stowNewFunc(box) }) } } else { continent.tasks ! GUIDTask.UnregisterObjectTask(previousBox)(continent.GUID) @@ -6516,8 +6577,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def NewItemDrop(obj: PlanetSideServerObject with Container, zone: Zone)(item: Equipment): TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { - private val localItem = item - private val localFunc: (Equipment) => Unit = NormalItemDrop(obj, zone) + private val localItem = item + private val localFunc: Equipment => Unit = NormalItemDrop(obj, zone) override def Description: String = s"dropping a new ${localItem.Definition.Name} on the ground" @@ -6541,13 +6602,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val ammoType = tool.AmmoType FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters case Nil => - log.info(s"no more $ammoType grenades") + log.info(s"${player.Name} has no more $ammoType grenades to throw") RemoveOldEquipmentFromInventory(player)(tool) case x :: xs => //this is similar to ReloadMessage val box = x.obj.asInstanceOf[Tool] val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[Tool].Magazine).reduce(_ + _) } + else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } val sumReloadValue: Int = box.Magazine + tailReloadValue val actualReloadValue = if (sumReloadValue <= 3) { RemoveOldEquipmentFromInventory(player)(x.obj) @@ -6556,7 +6617,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) 3 } - log.info(s"found $actualReloadValue more $ammoType grenades to throw") + log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") ModifyAmmunition(player)( tool.AmmoSlot.Box, -actualReloadValue @@ -6610,7 +6671,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } else { "" } - log.error(s"DeployRequest: $obj can not transition to $state - $reason$mobileShift") + log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift") } /** @@ -6737,7 +6798,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @see `Door` * @see `GenericObjectStateMsg` * @see `Hackable` - * @see `HackCaptureTerminal` * @see `HackObject` * @see `PlanetsideAttributeMessage` * @see `ResourceSilo` @@ -6918,7 +6978,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con *
* If that player is in a vehicle, it will construct that vehicle. * If the player is the driver of the vehicle, - * they must temporarily be removed from the driver seat in order for the vehicle to be constructed properly. + * they must temporarily be removed from the driver mount in order for the vehicle to be constructed properly. * These two previous statements operate through similar though distinct mechanisms and imply different conditions. * In reality, they produce the same output but enforce different relationships between the components. * The vehicle without a rendered player will always be created if that vehicle exists. @@ -6938,7 +6998,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //if the vehicle is the cargo of another vehicle in this zone val carrierInfo = continent.GUID(vehicle.MountedIn) match { case Some(carrier: Vehicle) => - (Some(carrier), carrier.CargoHolds.find({ case (index, hold) => hold.Occupant.contains(vehicle) })) + (Some(carrier), carrier.CargoHolds.find({ case (index, hold) => hold.occupant.contains(vehicle) })) case _ => (None, None) } @@ -6953,13 +7013,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.Transport ! Zone.Vehicle.Spawn(vehicle) //as the driver, we must temporarily exclude ourselves from being in the vehicle during its creation val seat = vehicle.Seats(0) - seat.Occupant = None + seat.unmount(player) + player.VehicleSeated = None val data = vdef.Packet.ConstructorData(vehicle).get sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, data)) - seat.Occupant = player + seat.mount(player) Vehicles.Own(vehicle, player) vehicle.CargoHolds.values - .collect { case hold if hold.isOccupied => hold.Occupant.get } + .collect { case hold if hold.isOccupied => hold.occupant.get } .foreach { _.MountedIn = vguid } continent.VehicleEvents ! VehicleServiceMessage( continent.id, @@ -6995,12 +7056,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con zone.id, VehicleAction.UnloadVehicle(player.GUID, vehicle, vehicleToDelete) ) - log.info( + log.debug( s"AvatarCreate: cleaning up ghost of transitioning vehicle ${vehicle.Definition.Name}@${vehicleToDelete.guid} in zone ${zone.id}" ) } Vehicles.ReloadAccessPermissions(vehicle, player.Name) - //log.info(s"AvatarCreate (vehicle): $guid -> $data") + log.debug(s"AvatarCreate (vehicle): ${player.Name}'s ${vehicle.Definition.Name}") + log.trace(s"AvatarCreate (vehicle): ${player.Name}'s ${vehicle.Definition.Name} - $vguid -> $vdata") AvatarCreateInVehicle(player, vehicle, seat) case _ => @@ -7013,8 +7075,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.id, AvatarAction.LoadPlayer(guid, ObjectClass.avatar, guid, packet.ConstructorData(player).get, None) ) - //log.info(s"AvatarCreate: $guid -> $data") - log.trace(s"AvatarCreate: ${player.Name}") + log.debug(s"AvatarCreate: ${player.Name}") + log.trace(s"AvatarCreate: ${player.Name} - $guid -> $data") } continent.Population ! Zone.Population.Spawn(avatar, player, avatarActor) //cautious redundancy @@ -7025,12 +7087,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * If the player is mounted in some entity, find that entity and get the seat index number at which the player is sat. + * If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat. * The priority of object confirmation is `direct` then `occupant.VehicleSeated`. * Once an object is found, the remainder are ignored. * @param direct a game object in which the player may be sat * @param occupant the player who is sat and may have specified the game object in which mounted - * @return a tuple consisting of a vehicle reference and a seat index + * @return a tuple consisting of a vehicle reference and a mount index * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ @@ -7052,7 +7114,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat.
+ * If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.
*
* For special purposes involved in zone transfers, * where the vehicle may or may not exist in either of the zones (yet), @@ -7061,7 +7123,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * to avoid inspecting the wrong vehicle and failing simple vehicle checks where this function may be employed. * @see `GetMountableAndSeat` * @see `interstellarFerry` - * @return a tuple consisting of a vehicle reference and a seat index + * @return a tuple consisting of a vehicle reference and a mount index * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ @@ -7072,9 +7134,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat. + * If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat. * @see `GetMountableAndSeat` - * @return a tuple consisting of a vehicle reference and a seat index + * @return a tuple consisting of a vehicle reference and a mount index * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) */ @@ -7085,7 +7147,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * Create an avatar character so that avatar's player is mounted in a vehicle's seat. + * Create an avatar character so that avatar's player is mounted in a vehicle's mount. * A part of the process of spawning the player into the game world.
*
* This is a very specific configuration of the player character that is not visited very often. @@ -7098,9 +7160,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * to avoid damaging the critical setup of this function. * @see `AccessContainer` * @see `UpdateWeaponAtSeatPosition` - * @param tplayer the player avatar seated in the vehicle's seat + * @param tplayer the player avatar seated in the vehicle's mount * @param vehicle the vehicle the player is riding - * @param seat the seat index + * @param seat the mount index */ def AvatarCreateInVehicle(tplayer: Player, vehicle: Vehicle, seat: Int): Unit = { val pdef = tplayer.avatar.definition @@ -7109,8 +7171,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con tplayer.VehicleSeated = None val pdata = pdef.Packet.DetailedConstructorData(tplayer).get tplayer.VehicleSeated = vguid + log.debug(s"AvatarCreateInVehicle: ${player.Name}") + log.trace(s"AvatarCreateInVehicle: ${player.Name} - $pguid -> $pdata") sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata)) - if (seat == 0 || vehicle.Seats(seat).ControlledWeapon.nonEmpty) { + if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) { sendResponse(ObjectAttachMessage(vguid, pguid, seat)) AccessContainer(vehicle) UpdateWeaponAtSeatPosition(vehicle, seat) @@ -7128,7 +7192,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con Some(ObjectCreateMessageParent(vguid, seat)) ) ) - //log.info(s"AvatarCreateInVehicle: $pguid -> pdata") } /** @@ -7145,7 +7208,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con *
* If that player is in a vehicle, it will construct that vehicle. * If the player is the driver of the vehicle, - * they must temporarily be removed from the driver seat in order for the vehicle to be constructed properly. + * they must temporarily be removed from the driver mount in order for the vehicle to be constructed properly. * These two previous statements operate through similar though distinct mechanisms and imply different conditions. * In reality, they produce the same output but enforce different relationships between the components. * The vehicle without a rendered player will always be created if that vehicle exists.
@@ -7164,18 +7227,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //vehicle and driver/passenger val vdef = vehicle.Definition val vguid = vehicle.GUID - if (seat == 0) { + val vdata = if (seat == 0) { val seat = vehicle.Seats(0) - seat.Occupant = None - val vdata = vdef.Packet.ConstructorData(vehicle).get - sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, vdata)) - seat.Occupant = player + seat.unmount(player) + val _vdata = vdef.Packet.ConstructorData(vehicle).get + sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, _vdata)) + seat.mount(player) + _vdata } else { - val vdata = vdef.Packet.ConstructorData(vehicle).get - sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, vdata)) + val _vdata = vdef.Packet.ConstructorData(vehicle).get + sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, _vdata)) + _vdata } Vehicles.ReloadAccessPermissions(vehicle, continent.id) - //log.info(s"AvatarCreate (vehicle): $vguid -> $vdata") + log.debug(s"AvatarCreate (vehicle): ${player.Name}'s ${vehicle.Definition.Name}") + log.trace(s"AvatarCreate (vehicle): ${player.Name}'s ${vehicle.Definition.Name} - $vguid -> $vdata") val pdef = player.avatar.definition val pguid = player.GUID val parent = ObjectCreateMessageParent(vguid, seat) @@ -7183,8 +7249,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val pdata = pdef.Packet.DetailedConstructorData(player).get player.VehicleSeated = vguid sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata)) - //log.info(s"AvatarRejoin: $vguid -> $vdata") - if (seat == 0 || vehicle.Seats(seat).ControlledWeapon.nonEmpty) { + log.debug(s"AvatarRejoin: ${player.Name} - $pguid -> $pdata") + if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) { sendResponse(ObjectAttachMessage(vguid, pguid, seat)) AccessContainer(vehicle) UpdateWeaponAtSeatPosition(vehicle, seat) @@ -7192,7 +7258,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con interimUngunnedVehicle = Some(vguid) interimUngunnedVehicleSeat = Some(seat) } - log.info(s"AvatarRejoin: ${player.Name} in ${vehicle.Definition.Name}") case _ => player.VehicleSeated = None @@ -7200,8 +7265,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val data = packet.DetailedConstructorData(player).get val guid = player.GUID sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data)) - //log.info(s"AvatarRejoin: $guid -> $data") - log.trace(s"AvatarRejoin: ${player.Name}") + log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") } //cautious redundancy deadState = DeadState.Alive @@ -7226,6 +7290,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val obj = Player.Respawn(tplayer) DefinitionUtil.applyDefaultLoadout(obj) obj.death_by = tplayer.death_by + obj.silenced = tplayer.silenced obj } @@ -7370,26 +7435,25 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def RequestSanctuaryZoneSpawn(tplayer: Player, currentZone: Int): Unit = { if (currentZone == Zones.sanctuaryZoneNumber(tplayer.Faction)) { - log.error("RequestSanctuaryZoneSpawn called for player already in sanctuary.") - sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn called for player already in sanctuary.")) - return - } - - continent.GUID(player.VehicleSeated) match { - case Some(obj: Vehicle) if !obj.Destroyed => - cluster ! InterstellarClusterService.GetRandomSpawnPoint( - Zones.sanctuaryZoneNumber(player.Faction), - player.Faction, - Seq(SpawnGroup.WarpGate), - context.self - ) - case _ => - cluster ! InterstellarClusterService.GetRandomSpawnPoint( - Zones.sanctuaryZoneNumber(player.Faction), - player.Faction, - Seq(SpawnGroup.Sanctuary), - context.self - ) + log.error(s"RequestSanctuaryZoneSpawn: ${player.Name} is already in faction sanctuary zone.") + sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn: player is already in sanctuary.")) + } else { + continent.GUID(player.VehicleSeated) match { + case Some(obj : Vehicle) if !obj.Destroyed => + cluster ! ICS.GetRandomSpawnPoint( + Zones.sanctuaryZoneNumber(player.Faction), + player.Faction, + Seq(SpawnGroup.WarpGate), + context.self + ) + case _ => + cluster ! ICS.GetRandomSpawnPoint( + Zones.sanctuaryZoneNumber(player.Faction), + player.Faction, + Seq(SpawnGroup.Sanctuary), + context.self + ) + } } } @@ -7441,7 +7505,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _: Vehicle => terminal.Actor ! CommonMessages.Use(player, Some((target, continent.VehicleEvents))) case _ => - log.error(s"StartUsingProximityUnit: can not deal with target $target") + log.error(s"StartUsingProximityUnit: ${player.Name}, this ${terminal.Definition.Name} can not deal with target $target") } terminal.Definition match { case GlobalDefinitions.adv_med_terminal | GlobalDefinitions.medical_terminal => @@ -7474,7 +7538,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def StopUsingProximityUnit(terminal: Terminal with ProximityUnit): Unit = { val term_guid = terminal.GUID - //log.trace(s"StopUsingProximityUnit: attempting to stop using proximity unit ${terminal.Definition.Name}@${term_guid.guid}") val targets = FindProximityUnitTargetsInScope(terminal) if (targets.nonEmpty) { if (usingMedicalTerminal.contains(term_guid)) { @@ -7703,7 +7766,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (0 <= index && index < projectiles.length) { projectiles(index) } else { - log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") + log.trace(s"ResolveProjectile: ${player.Name} expected projectile, but ${projectile_guid.guid} not found") None } } @@ -7725,7 +7788,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(projectile) => ResolveProjectileInteraction(projectile, resolution, target, pos) case None => - log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") + log.trace(s"ResolveProjectile: ${player.Name} expected projectile, but ${projectile_guid.guid} not found") None } } @@ -7765,7 +7828,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con pos: Vector3 ): Option[DamageInteraction] = { if (projectile.isMiss) { - log.error("expected projectile was already counted as a missed shot; can not resolve any further") + log.warn("expected projectile was already counted as a missed shot; can not resolve any further") None } else { projectile.Resolve() @@ -7790,7 +7853,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Common activities/procedure when a player mounts a valid object. * @param tplayer the player * @param obj the mountable object - * @param seatNum the seat into which the player is mounting + * @param seatNum the mount into which the player is mounting */ def MountingAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { val player_guid: PlanetSideGUID = tplayer.GUID @@ -7798,7 +7861,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con PlayerActionsToCancel() avatarActor ! AvatarActor.DeactivateActiveImplants() avatarActor ! AvatarActor.SuspendStaminaRegeneration(3 seconds) - log.info(s"MountVehicleMsg: ${player.Name}_guid mounts $obj @ $seatNum") sendResponse(ObjectAttachMessage(obj_guid, player_guid, seatNum)) continent.VehicleEvents ! VehicleServiceMessage( continent.id, @@ -7810,13 +7872,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Common activities/procedure when a player dismounts a valid object. * @param tplayer the player * @param obj the mountable object - * @param seatNum the seat out of which which the player is disembarking + * @param seatNum the mount out of which which the player is disembarking */ def DismountAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { val player_guid: PlanetSideGUID = tplayer.GUID - log.info(s"DismountVehicleMsg: ${tplayer.Name} dismounts $obj from $seatNum") + log.info( + s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name} from seat #$seatNum" + ) keepAliveFunc = NormalKeepAlive - sendResponse(DismountVehicleMsg(player_guid, BailType.Normal, false)) + sendResponse(DismountVehicleMsg(player_guid, BailType.Normal, wasKickedByDriver = false)) continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.DismountVehicle(player_guid, BailType.Normal, false) @@ -7839,13 +7903,16 @@ 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}") // auto kick players damaging spectators if (obj.spectator && obj != player) { AdministrativeKick(player) } else { obj.Actor ! Vitality.Damage(func) } - case obj: Vehicle if obj.CanDamage => 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}") + obj.Actor ! Vitality.Damage(func) case obj: Amenity if obj.CanDamage => obj.Actor ! Vitality.Damage(func) case obj: ComplexDeployable if obj.CanDamage => obj.Actor ! Vitality.Damage(func) @@ -7990,11 +8057,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def NextConstructionItemFireMode(obj: ConstructionItem, originalModeIndex: Int): ConstructionFireMode = { do { obj.NextFireMode - if (!ConstructionItemPermissionComparison(player.avatar.certifications, obj.ModePermissions)) { + if (!Deployables.constructionItemPermissionComparison(player.avatar.certifications, obj.ModePermissions)) { PerformConstructionItemAmmoChange(obj, obj.AmmoTypeIndex) } sendResponse(ChangeFireModeMessage(obj.GUID, obj.FireModeIndex)) - } while (!ConstructionItemPermissionComparison( + } while (!Deployables.constructionItemPermissionComparison( player.avatar.certifications, obj.ModePermissions ) && originalModeIndex != obj.FireModeIndex) @@ -8013,45 +8080,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def PerformConstructionItemAmmoChange(obj: ConstructionItem, originalAmmoIndex: Int): Unit = { do { obj.NextAmmoType - } while (!ConstructionItemPermissionComparison( + } while (!Deployables.constructionItemPermissionComparison( player.avatar.certifications, obj.ModePermissions ) && originalAmmoIndex != obj.AmmoTypeIndex) log.info( - s"ChangeFireMode: construction object ${obj.Definition.Name} changed to ${obj.AmmoType} (mode ${obj.FireModeIndex})" + s"${player.Name} switched construction object ${obj.Definition.Name} to ${obj.AmmoType} (mode #${obj.FireModeIndex})" ) sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) } - /** - * Compare sets of certifications to determine if - * the requested `Engineering`-like certification requirements of the one group can be found in a another group. - * @see `CertificationType` - * @param sample the certifications to be compared against - * @param test the desired certifications - * @return `true`, if the desired certification requirements are met; `false`, otherwise - */ - def ConstructionItemPermissionComparison( - sample: Set[Certification], - test: Set[Certification] - ): Boolean = { - import Certification._ - val engineeringCerts: Set[Certification] = Set(AssaultEngineering, FortificationEngineering) - val testDiff: Set[Certification] = test diff (engineeringCerts ++ Set(AdvancedEngineering)) - //substitute `AssaultEngineering` and `FortificationEngineering` for `AdvancedEngineering` - val sampleIntersect = if (sample contains AdvancedEngineering) { - engineeringCerts - } else { - sample intersect engineeringCerts - } - val testIntersect = if (test contains AdvancedEngineering) { - engineeringCerts - } else { - test intersect engineeringCerts - } - (sample intersect testDiff equals testDiff) && (sampleIntersect intersect testIntersect equals testIntersect) - } - /** * Common actions related to constructing a new `Deployable` object in the game environment.
*
@@ -8080,7 +8118,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val item = definition.Item val deployables = avatar.deployables val (curr, max) = deployables.CountDeployable(item) - log.info(s"DeployableBuildActivity: ${definition.Name}") //two potential messages related to numerical limitations of deployables if (!avatar.deployables.Available(obj)) { val (removed, msg) = { @@ -8108,7 +8145,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case None => ; //should be an invalid case log.warn( - s"DeployableBuildActivity: how awkward: we probably shouldn't be allowed to build this deployable right now" + s"DeployableBuildActivity: how awkward: ${player.Name} probably shouldn't be allowed to build this deployable right now" ) } } else if (obj.isInstanceOf[TelepadDeployable]) { @@ -8191,11 +8228,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } else { player.Find(tool) match { case Some(newIndex) => - log.warn(s"$logDecorator: looking for item in $index, but item was found at $newIndex instead") + log.warn(s"$logDecorator: ${player.Name} was looking for an item in his hand $index, but item was found at $newIndex instead") player.Slot(newIndex).Equipment = None true case None => - log.error(s"$logDecorator: could not find the target ${tool.Definition.Name}") + log.warn(s"$logDecorator: ${player.Name} could not find the target ${tool.Definition.Name}") false } } @@ -8229,7 +8266,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val definition = tool.Definition if (player.Slot(index).Equipment.isEmpty) { - FindEquipmentStock(player, { (e) => e.Definition == definition }, 1) match { + FindEquipmentStock(player, { e => e.Definition == definition }, 1) match { case x :: _ => val guid = player.GUID val obj = x.obj.asInstanceOf[ConstructionItem] @@ -8261,7 +8298,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } else { log.warn( - s"FindReplacementConstructionItem: slot $index needs to be empty before a replacement ${definition.Name} can be installed" + s"FindReplacementConstructionItem: ${player.Name}, your $index hand needs to be empty before a replacement ${definition.Name} can be installed" ) } } @@ -8301,22 +8338,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some((parent, Some(slot))) => obj.Position = Vector3.Zero RemoveOldEquipmentFromInventory(parent)(obj) - log.info(s"RequestDestroy: equipment $obj") true case _ => if (player.avatar.locker.Inventory.Remove(object_guid)) { sendResponse(ObjectDeleteMessage(object_guid, 0)) - log.info(s"RequestDestroy: equipment $obj") true } else if (continent.EquipmentOnGround.contains(obj)) { obj.Position = Vector3.Zero continent.Ground ! Zone.Ground.RemoveItem(object_guid) continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) - log.info(s"RequestDestroy: equipment $obj on ground") true } else { - log.warn(s"RequestDestroy: equipment $obj exists, but can not be reached") + log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it") false } } @@ -8424,7 +8458,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * does not factor in any time required for loading zone or game objects */ def LoadZonePhysicalSpawnPoint(zoneId: String, pos: Vector3, ori: Vector3, respawnTime: FiniteDuration): Unit = { - log.info(s"Load in zone $zoneId at position $pos in $respawnTime") + log.info(s"${player.Name} will load in zone $zoneId at position $pos in $respawnTime") respawnTimer.cancel() reviveTimer.cancel() deadState = DeadState.RespawnTime @@ -8488,7 +8522,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param zoneId the zone in which the player will be placed */ def LoadZoneAsPlayer(targetPlayer: Player, zoneId: String): Unit = { - log.debug(s"loadZoneAsPlayer ${targetPlayer.avatar.name} $zoneId") + log.debug(s"LoadZoneAsPlayer: ${targetPlayer.avatar.name} loading into $zoneId") if (!zoneReload && zoneId == continent.id) { if (player.isBackpack) { // important! test the actor-wide player ref, not the parameter // respawning from unregistered player @@ -8504,15 +8538,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con session = session.copy(player = targetPlayer) taskThenZoneChange( GUIDTask.UnregisterObjectTask(original.avatar.locker)(continent.GUID), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } else if (player.HasGUID) { taskThenZoneChange( GUIDTask.UnregisterAvatar(original)(continent.GUID), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } else { - cluster ! InterstellarClusterService.FindZone(_.id == zoneId, context.self) + cluster ! ICS.FindZone(_.id == zoneId, context.self) } } @@ -8569,9 +8603,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @return a tuple composed of an `ActorRef` destination and a message to send to that destination */ def LoadZoneInVehicleAsDriver(vehicle: Vehicle, zoneId: String): Unit = { - log.info(s"LoadZoneInVehicleAsDriver: ${player.Name} is driving a ${vehicle.Definition.Name}") + val msg: String = s"${player.Name} is driving a ${vehicle.Definition.Name}" + log.info(msg) + log.debug(s"LoadZoneInVehicleAsDriver: $msg") val manifest = vehicle.PrepareGatingManifest() - log.info(s"$manifest") val pguid = player.GUID val toChannel = manifest.file val topLevel = interstellarFerryTopLevelGUID.getOrElse(vehicle.GUID) @@ -8581,11 +8616,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) manifest.cargo.foreach { case ("MISSING_DRIVER", index) => - val cargo = vehicle.CargoHolds(index).Occupant.get - log.error(s"LoadZoneInVehicleAsDriver: eject cargo in hold $index; vehicle missing driver") + val cargo = vehicle.CargoHolds(index).occupant.get + 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) case (name, index) => - val cargo = vehicle.CargoHolds(index).Occupant.get + val cargo = vehicle.CargoHolds(index).occupant.get continent.VehicleEvents ! VehicleServiceMessage( name, VehicleAction.TransferPassengerChannel(pguid, s"${cargo.Actor}", toChannel, cargo, topLevel) @@ -8606,7 +8641,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.Continent = zoneId //forward-set the continent id to perform a test taskThenZoneChange( GUIDTask.UnregisterAvatar(player)(continent.GUID), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } else { UnaccessContainer(vehicle) @@ -8630,7 +8665,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.Transport ! Zone.Vehicle.Despawn(vehicle) taskThenZoneChange( UnregisterDrivenVehicle(vehicle, player), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } } @@ -8661,7 +8696,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @return a tuple composed of an `ActorRef` destination and a message to send to that destination */ def LoadZoneInVehicleAsPassenger(vehicle: Vehicle, zoneId: String): Unit = { - log.info(s"LoadZoneInVehicleAsPassenger: ${player.Name} is the passenger of a ${vehicle.Definition.Name}") + val msg: String = s"${player.Name} is the passenger of a ${vehicle.Definition.Name}" + log.info(msg) + log.debug(s"LoadZoneInVehicleAsPassenger: $msg") if (!zoneReload && zoneId == continent.id) { //transferring a vehicle between spawn points (warp gates) in the same zone self ! PlayerLoaded(player) @@ -8674,7 +8711,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con taskThenZoneChange( GUIDTask.UnregisterAvatar(player)(continent.GUID), - InterstellarClusterService.FindZone(_.id == zoneId, context.self) + ICS.FindZone(_.id == zoneId, context.self) ) } } @@ -8701,7 +8738,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con vehicle.CargoHolds.values .collect { case hold if hold.isOccupied => - val cargo = hold.Occupant.get + val cargo = hold.occupant.get cargo.Continent = toZoneId //point to the cargo vehicle to instigate cargo vehicle driver transportation galaxyService ! GalaxyServiceMessage( @@ -8710,14 +8747,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } case None => - log.error("LoadZoneTransferPassengerMessages: expected a manifest for zone transfer; got nothing") + log.error(s"LoadZoneTransferPassengerMessages: ${player.Name} expected a manifest for zone transfer; got nothing") } } /** Before changing zones, perform the following task (which can be a nesting of subtasks). */ def taskThenZoneChange( task: TaskResolver.GiveTask, - zoneMessage: InterstellarClusterService.FindZone + zoneMessage: ICS.FindZone ): Unit = { continent.tasks ! TaskResolver.GiveTask( new Task() { @@ -8871,7 +8908,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid) ) } else { - log.warn(s"UseRouterTelepadSystem: can not teleport") + log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport") } recentTeleportAttempt = time } @@ -8932,14 +8969,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } /** - * From a seat, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines. + * From a mount, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines. * @param objWithSeat the object that owns seats (and weaponry) - * @param seatNum the seat + * @param seatNum the mount */ - def UpdateWeaponAtSeatPosition(objWithSeat: MountedWeapons, seatNum: Int): Unit = { + def UpdateWeaponAtSeatPosition(objWithSeat: MountableWeapons, seatNum: Int): Unit = { objWithSeat.WeaponControlledFromSeat(seatNum) match { case Some(weapon: Tool) => - //update mounted weapon belonging to seat + //update mounted weapon belonging to mount weapon.AmmoSlots.foreach(slot => { //update the magazine(s) in the weapon, specifically val magazine = slot.Box @@ -8983,7 +9020,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con .sortBy(tube => Vector3.DistanceSquared(tube.Position, player.Position)) .headOption match { case Some(tube) => - log.info("DrawCurrentAmsSpawnPoint - new @ams spawn point drawn") sendResponse( BindPlayerMessage( BindStatus.Available, @@ -8997,7 +9033,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) case None => - log.info("DrawCurrentAmsSpawnPoint - no @ams spawn point drawn") sendResponse( BindPlayerMessage( BindStatus.Unavailable, @@ -9147,7 +9182,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case PlanetSideEmpire.NC => if (player.Capacitor > 0) player.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded case _ => - log.warn(s"Player ${player.Name} tried to use a MAX special ability but their faction doesn't have one") + log.warn(s"${player.Name} tried to use a MAX special ability but their faction doesn't have one") } if ( player.UsingSpecial == SpecialExoSuitDefinition.Mode.Overdrive || player.UsingSpecial == SpecialExoSuitDefinition.Mode.Shielded @@ -9181,9 +9216,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con .flatMap { case Some(obj: Vehicle) if !obj.Cloaked => //TODO hint: vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.ProjectileAutoLockAwareness(mode)) - obj.Seats.values.collect { case seat if seat.isOccupied => seat.Occupant.get.Name } + obj.Seats.values.flatMap { case seat => seat.occupants.map(_.Name) } case Some(obj: Mountable) => - obj.Seats.values.collect { case seat if seat.isOccupied => seat.Occupant.get.Name } + obj.Seats.values.flatMap { case seat => seat.occupants.map(_.Name) } case Some(obj: Player) if obj.ExoSuit == ExoSuitType.MAX => Seq(obj.Name) case _ => @@ -9245,7 +9280,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) { // If the target position on the server does not match the position where the projectile landed within reason there may be foul play log.warn( - s"Shot guid $projectile_guid has hit location discrepancy with target location. Target: ${target.Position} Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" + s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" ) } } @@ -9290,7 +9325,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def TurnCounterDuringInterim(guid: PlanetSideGUID): Unit = { upstreamMessageCount = 0 - if (player.GUID == guid && player.Zone == continent) { + if (player != null && player.GUID == guid && player.Zone == continent) { turnCounterFunc = NormalTurnCounter } } @@ -9303,15 +9338,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Until new upstream messages that pass some tests against their data start being reported, * the counter does not accumulate properly.
*
- * In the case that the transitioning player is seated in a vehicle seat + * In the case that the transitioning player is seated in a vehicle mount * that is not the driver and does not have a mounted weapon under its control, * no obvious feedback will be provided by the client. * For example, when as infantry, a `PlayerStateMessageUpstream` packet is dispatched by the client. - * For example, when in the driver seat, a `VehicleStateMessage` is dispatched by the client. + * For example, when in the driver mount, a `VehicleStateMessage` is dispatched by the client. * In the given case, the only packet that indicates the player is seated is a `KeepAliveMessage`. * Detection of this `KeepALiveMessage`, for the purpose of transitioning logic, * can not be instantaneous to the zoning process or other checks for proper zoning conditions that will be disrupted. - * To avoid complications, the player in such a seat is initially spawned as infantry on their own client, + * To avoid complications, the player in such a mount is initially spawned as infantry on their own client, * realizes the state transition confirmation for infantry (turn counter), * and is forced to transition into being seated, * and only at that time will begin registering `KeepAliveMessage` to mark the end of their interim period. @@ -9352,27 +9387,27 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * The atypical response to receiving a `KeepAliveMessage` packet from the client.
*
* `KeepAliveMessage` packets are the primary vehicle for persistence due to client reporting - * in the case where the player's avatar is riding in a vehicle in a seat with no vehicle. + * in the case where the player's avatar is riding in a vehicle in a mount with no vehicle. * @see `KeepAliveMessage` * @see `keepAliveFunc` * @see `turnCounterFunc` * @see `persist` */ def KeepAlivePersistence(): Unit = { - //log.info(s"KeepAlive in a vehicle - $upstreamMessageCount") interimUngunnedVehicle = None persist() turnCounterFunc(player.GUID) } def AdministrativeKick(tplayer: Player) = { + log.warn(s"${tplayer.Name} has been kicked by ${player.Name}") tplayer.death_by = -1 accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) //get out of that vehicle GetMountableAndSeat(None, tplayer, continent) match { case (Some(obj), Some(seatNum)) => tplayer.VehicleSeated = None - obj.Seats(seatNum).Occupant = None + obj.Seats(seatNum).unmount(tplayer) continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.KickPassenger(tplayer.GUID, seatNum, false, obj.GUID) @@ -9383,8 +9418,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def KickedByAdministration(): Unit = { sendResponse(DisconnectMessage("@kick_w")) - Thread.sleep(300) - middlewareActor ! MiddlewareActor.Teardown() + context.system.scheduler.scheduleOnce( + delay = 300 milliseconds, + middlewareActor.toClassic, + MiddlewareActor.Teardown() + ) } def ImmediateDisconnect(): Unit = { @@ -9405,8 +9443,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None => false } ) { - log.trace( - s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}" + log.debug( + s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}, known to ${player.Name}" ) } val (angle, attribution, acceptableDistanceToOwner) = obj match { @@ -9451,7 +9489,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con projectiles(projectileIndex) = Some(projectile.quality(initialQuality)) if (projectile_info.ExistsOnRemoteClients) { log.trace( - s"WeaponFireMessage: ${projectile_info.Name} is a remote projectile" + s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile" ) continent.tasks ! (if (projectile.HasGUID) { continent.AvatarEvents ! AvatarServiceMessage( diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index 6afa0c96..9b65a341 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -205,7 +205,10 @@ class BuildingActor( case AmenityStateChange(terminal: CaptureTerminal, data) => // Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => { - amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, data.get.asInstanceOf[Boolean]) + data match { + case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured) + case _ => log.warn("CaptureTerminal AmenityStateChange was received with no attached data.") + } }) // When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state diff --git a/src/main/scala/net/psforever/login/TcpListener.scala b/src/main/scala/net/psforever/login/TcpListener.scala index 1a65c966..820ba79f 100644 --- a/src/main/scala/net/psforever/login/TcpListener.scala +++ b/src/main/scala/net/psforever/login/TcpListener.scala @@ -26,7 +26,7 @@ class TcpListener[T <: Actor](actorClass: Class[T], nextActorName: String, liste def receive = { case Tcp.Bound(local) => - log.info(s"Now listening on TCP:$local") + log.debug(s"Now listening on TCP:$local") context.become(ready(sender())) case Tcp.CommandFailed(Tcp.Bind(_, address, _, _, _)) => diff --git a/src/main/scala/net/psforever/objects/Deployables.scala b/src/main/scala/net/psforever/objects/Deployables.scala index 704eb076..3799c18c 100644 --- a/src/main/scala/net/psforever/objects/Deployables.scala +++ b/src/main/scala/net/psforever/objects/Deployables.scala @@ -2,7 +2,7 @@ package net.psforever.objects import akka.actor.ActorRef -import net.psforever.objects.avatar.Avatar +import net.psforever.objects.avatar.{Avatar, Certification} import scala.concurrent.duration._ import net.psforever.objects.ce.{Deployable, DeployedItem} @@ -13,7 +13,7 @@ import net.psforever.services.RemoverActor import net.psforever.services.local.{LocalAction, LocalServiceMessage} object Deployables { - private val log = org.log4s.getLogger("Deployables") + //private val log = org.log4s.getLogger("Deployables") object Make { def apply(item: DeployedItem.Value): () => PlanetSideGameObject with Deployable = cemap(item) @@ -128,7 +128,6 @@ object Deployables { * @param avatar the player's core */ def InitializeDeployableQuantities(avatar: Avatar): Boolean = { - log.info("Setting up combat engineering ...") avatar.deployables.Initialize(avatar.certifications) } @@ -137,8 +136,35 @@ object Deployables { * @param avatar the player's core */ def InitializeDeployableUIElements(avatar: Avatar): List[(Int, Int, Int, Int)] = { - log.info("Setting up combat engineering UI ...") avatar.deployables.UpdateUI() } + /** + * Compare sets of certifications to determine if + * the requested `Engineering`-like certification requirements of the one group can be found in a another group. + * @see `CertificationType` + * @param sample the certifications to be compared against + * @param test the desired certifications + * @return `true`, if the desired certification requirements are met; `false`, otherwise + */ + def constructionItemPermissionComparison( + sample: Set[Certification], + test: Set[Certification] + ): Boolean = { + import Certification._ + val engineeringCerts: Set[Certification] = Set(AssaultEngineering, FortificationEngineering) + val testDiff: Set[Certification] = test diff (engineeringCerts ++ Set(AdvancedEngineering)) + //substitute `AssaultEngineering` and `FortificationEngineering` for `AdvancedEngineering` + val sampleIntersect = if (sample contains AdvancedEngineering) { + engineeringCerts + } else { + sample intersect engineeringCerts + } + val testIntersect = if (test contains AdvancedEngineering) { + engineeringCerts + } else { + test intersect engineeringCerts + } + (sample intersect testDiff equals testDiff) && (sampleIntersect intersect testIntersect equals testIntersect) + } } diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 8b749a60..fc3c00a3 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -2,16 +2,20 @@ package net.psforever.objects import akka.actor.{Actor, ActorContext, Props} +import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} import net.psforever.objects.ce._ import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition} import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.equipment.JammableUnit -import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.geometry.Geometry3D +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.vital.resolution.ResolutionCalculations.Output -import net.psforever.objects.vital.SimpleResolutions -import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.vital.{SimpleResolutions, Vitality} +import net.psforever.objects.vital.etc.TriggerUsedReason +import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.Zone import net.psforever.types.Vector3 @@ -21,7 +25,9 @@ import net.psforever.services.local.{LocalAction, LocalServiceMessage} import scala.concurrent.duration._ -class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition) extends ComplexDeployable(cdef) with JammableUnit { +class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition) + extends ComplexDeployable(cdef) + with JammableUnit { override def Definition: ExplosiveDeployableDefinition = cdef } @@ -63,6 +69,24 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D def receive: Receive = takesDamage .orElse { + case CommonMessages.Use(player, Some(trigger: BoomerTrigger)) if { + mine match { + case boomer: BoomerDeployable => boomer.Trigger.contains(trigger) && mine.Definition.Damageable + case _ => false + } + } => + // the trigger damages the mine, which sets it off, which causes an explosion + // think of this as an initiator to the proper explosion + mine.Destroyed = true + ExplosiveDeployableControl.DamageResolution( + mine, + DamageInteraction( + SourceEntry(mine), + TriggerUsedReason(PlayerSource(player), trigger.GUID), + mine.Position + ).calculate()(mine), + damage = 0 + ) case _ => ; } @@ -74,19 +98,48 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D val originalHealth = mine.Health val cause = applyDamageTo(mine) val damage = originalHealth - mine.Health - if (Damageable.CanDamageOrJammer(mine, damage, cause.interaction)) { + if (CanDetonate(mine, damage, cause.interaction)) { ExplosiveDeployableControl.DamageResolution(mine, cause, damage) } else { mine.Health = originalHealth } } } + + /** + * A supplement for checking target susceptibility + * to account for sympathetic explosives even if there is no damage. + * This does not supercede other underlying checks or undo prior damage checks. + * @see `Damageable.CanDamageOrJammer` + * @see `DamageProperties.SympatheticExplosives` + * @param obj the entity being damaged + * @param damage the amount of damage + * @param data historical information about the damage + * @return `true`, if the target can be affected; + * `false`, otherwise + */ + def CanDetonate(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = { + !mine.Destroyed && (if (damage == 0 && data.cause.source.SympatheticExplosion) { + Damageable.CanDamageOrJammer(mine, damage = 1, data) + } else { + Damageable.CanDamageOrJammer(mine, damage, data) + }) + } } object ExplosiveDeployableControl { + /** + * na + * @param target na + * @param cause na + * @param damage na + */ def DamageResolution(target: ExplosiveDeployable, cause: DamageResult, damage: Int): Unit = { target.History(cause) - if (target.Health == 0) { + if (cause.interaction.cause.source.SympatheticExplosion) { + explodes(target, cause) + DestructionAwareness(target, cause) + } else if (target.Health == 0) { DestructionAwareness(target, cause) } else if (!target.Jammed && Damageable.CanJammer(target, cause.interaction)) { if ( { @@ -99,17 +152,27 @@ object ExplosiveDeployableControl { } } ) { - if (cause.interaction.cause.source.SympatheticExplosion || target.Definition.DetonateOnJamming) { - val zone = target.Zone - zone.Activity ! Zone.HotSpot.Activity(cause) - zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target)) - Zone.causeExplosion(zone, target, Some(cause)) + if (target.Definition.DetonateOnJamming) { + explodes(target, cause) } DestructionAwareness(target, cause) } } } + /** + * na + * @param target na + * @param cause na + */ + def explodes(target: Damageable.Target, cause: DamageResult): Unit = { + target.Health = 1 // short-circuit logic in DestructionAwareness + val zone = target.Zone + zone.Activity ! Zone.HotSpot.Activity(cause) + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target)) + Zone.causeExplosion(zone, target, Some(cause), ExplosiveDeployableControl.detectionForExplosiveSource(target)) + } + /** * na * @param target na @@ -118,8 +181,11 @@ object ExplosiveDeployableControl { def DestructionAwareness(target: ExplosiveDeployable, cause: DamageResult): Unit = { val zone = target.Zone val attribution = DamageableEntity.attributionTo(cause, target.Zone) + Deployables.AnnounceDestroyDeployable( + target, + Some(if (target.Jammed || target.Destroyed) 0 seconds else 500 milliseconds) + ) target.Destroyed = true - Deployables.AnnounceDestroyDeployable(target, Some(if (target.Jammed) 0 seconds else 500 milliseconds)) zone.AvatarEvents ! AvatarServiceMessage( zone.id, AvatarAction.Destroy(target.GUID, attribution, Service.defaultPlayerGUID, target.Position) @@ -131,4 +197,53 @@ object ExplosiveDeployableControl { ) } } + + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * For explosives, the source of the explosion is always typically constant. + * @see `detectsTarget` + * @see `ObjectDefinition.Geometry` + * @see `Vector3.relativeUp` + * @param obj a game entity that explodes + * @return a function that resolves a potential target as detected + */ + def detectionForExplosiveSource(obj: PlanetSideGameObject): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = { + val up = Vector3.relativeUp(obj.Orientation) //check relativeUp; rotate as little as necessary! + val g1 = obj.Definition.Geometry(obj) + detectTarget(g1, up) + } + + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * For explosives, targets in the damage radius in the direction of the blast (above the explosive) are valid targets. + * Targets that are ~0.5916f units in the opposite direction of the blast (below the explosive) are also selected. + * @see `ObjectDefinition.Geometry` + * @see `PrimitiveGeometry.pointOnOutside` + * @see `Vector3.DistanceSquared` + * @see `Vector3.neg` + * @see `Vector3.relativeUp` + * @see `Vector3.ScalarProjection` + * @see `Vector3.Unit` + * @param g1 a cached geometric representation that should belong to `obj1` + * @param up a cached vector in the direction of "above `obj1`'s geometric representation" + * @param obj1 a game entity that explodes + * @param obj2 a game entity that suffers the explosion + * @param maxDistance the square of the maximum distance permissible between game entities + * before they are no longer considered "near" + * @return `true`, if the target entities are near enough to each other; + * `false`, otherwise + */ + def detectTarget(g1: Geometry3D, up: Vector3)(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float) : Boolean = { + val g2 = obj2.Definition.Geometry(obj2) + val dir = g2.center.asVector3 - g1.center.asVector3 + //val scalar = Vector3.ScalarProjection(dir, up) + val point1 = g1.pointOnOutside(dir).asVector3 + val point2 = g2.pointOnOutside(Vector3.neg(dir)).asVector3 + val scalar = Vector3.ScalarProjection(point2 - point1, up) + (scalar >= 0 || Vector3.MagnitudeSquared(up * scalar) < 0.35f) && + math.min( + Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3), + Vector3.DistanceSquared(point1, point2) + ) <= maxDistance + } } diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 6861b335..d768f0a2 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -7,24 +7,27 @@ import net.psforever.objects.ce.{DeployableCategory, DeployedItem} import net.psforever.objects.definition._ import net.psforever.objects.definition.converter._ import net.psforever.objects.equipment._ +import net.psforever.objects.geometry.GeometryForm import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.serverobject.aura.Aura import net.psforever.objects.serverobject.doors.DoorDefinition import net.psforever.objects.serverobject.generator.GeneratorDefinition import net.psforever.objects.serverobject.locks.IFFLockDefinition import net.psforever.objects.serverobject.mblocker.LockerDefinition +import net.psforever.objects.serverobject.mount._ import net.psforever.objects.serverobject.pad.VehicleSpawnPadDefinition import net.psforever.objects.serverobject.painbox.PainboxDefinition import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.tube.SpawnTubeDefinition import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition -import net.psforever.objects.serverobject.structures.{AutoRepairStats, BuildingDefinition, WarpGateDefinition} +import net.psforever.objects.serverobject.structures.{AmenityDefinition, AutoRepairStats, BuildingDefinition, WarpGateDefinition} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition} import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, TurretUpgrade} -import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, SeatArmorRestriction, UtilityType} +import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType} import net.psforever.objects.vital.base.DamageType 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} @@ -937,6 +940,8 @@ object GlobalDefinitions { val phantasm = VehicleDefinition(ObjectClass.phantasm) val droppod = VehicleDefinition(ObjectClass.droppod) + + val orbital_shuttle = VehicleDefinition(ObjectClass.orbital_shuttle) init_vehicles() /* @@ -972,6 +977,8 @@ object GlobalDefinitions { val router_telepad_deployable = SimpleDeployableDefinition(DeployedItem.router_telepad_deployable) + val special_emp = ExplosiveDeployableDefinition(DeployedItem.jammer_mine) + //this is only treated like a deployable val internal_router_telepad_deployable = InternalTelepadDefinition() //objectId: 744 init_deployables() @@ -1053,6 +1060,8 @@ object GlobalDefinitions { val door = new DoorDefinition + val gr_door_mb_orb = new DoorDefinition + val resource_silo = new ResourceSiloDefinition val capture_terminal = new CaptureTerminalDefinition(158) // Base CC @@ -1095,6 +1104,8 @@ object GlobalDefinitions { val gen_control = new GeneratorTerminalDefinition(349) val generator = new GeneratorDefinition(351) + + val obbasemesh = new AmenityDefinition(598) { } initMiscellaneous() /* @@ -1641,7 +1652,7 @@ object GlobalDefinitions { 1.093750f // same regardless of gender } else if (p.ExoSuit == ExoSuitType.MAX) { 1.906250f // VS female MAX - } else if (p.Sex == CharacterGender.Male) { + } else if (p.Sex == CharacterSex.Male) { obj.Definition.MaxDepth // male } else { 1.546875f // female @@ -5608,18 +5619,29 @@ object GlobalDefinitions { * Initialize `VehicleDefinition` globals. */ private def init_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 + } + val maxOnlySeat = new SeatDefinition() { + restriction = MaxOnly + } + fury.Name = "fury" fury.MaxHealth = 650 fury.Damageable = true fury.Repairable = true fury.RepairIfDestroyed = false fury.MaxShields = 130 - fury.Seats += 0 -> new SeatDefinition() - fury.Seats(0).Bailable = true - fury.Seats(0).ControlledWeapon = 1 + fury.Seats += 0 -> bailableSeat + fury.controlledWeapons += 0 -> 1 fury.Weapons += 1 -> fury_weapon_systema - fury.MountPoints += 1 -> 0 - fury.MountPoints += 2 -> 0 + fury.MountPoints += 1 -> MountInfo(0) + fury.MountPoints += 2 -> MountInfo(0) fury.TrunkSize = InventoryTile.Tile1111 fury.TrunkOffset = 30 fury.TrunkLocation = Vector3(-1.71f, 0f, 0f) @@ -5633,11 +5655,12 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } fury.DrownAtMaxDepth = true fury.MaxDepth = 1.3f fury.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + fury.Geometry = atvForm quadassault.Name = "quadassault" // Basilisk quadassault.MaxHealth = 650 @@ -5645,12 +5668,11 @@ object GlobalDefinitions { quadassault.Repairable = true quadassault.RepairIfDestroyed = false quadassault.MaxShields = 130 - quadassault.Seats += 0 -> new SeatDefinition() - quadassault.Seats(0).Bailable = true - quadassault.Seats(0).ControlledWeapon = 1 + quadassault.Seats += 0 -> bailableSeat + quadassault.controlledWeapons += 0 -> 1 quadassault.Weapons += 1 -> quadassault_weapon_system - quadassault.MountPoints += 1 -> 0 - quadassault.MountPoints += 2 -> 0 + quadassault.MountPoints += 1 -> MountInfo(0) + quadassault.MountPoints += 2 -> MountInfo(0) quadassault.TrunkSize = InventoryTile.Tile1111 quadassault.TrunkOffset = 30 quadassault.TrunkLocation = Vector3(-1.71f, 0f, 0f) @@ -5664,11 +5686,12 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } quadassault.DrownAtMaxDepth = true quadassault.MaxDepth = 1.3f quadassault.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + quadassault.Geometry = atvForm quadstealth.Name = "quadstealth" // Wraith quadstealth.MaxHealth = 650 @@ -5677,11 +5700,10 @@ object GlobalDefinitions { quadstealth.RepairIfDestroyed = false quadstealth.MaxShields = 130 quadstealth.CanCloak = true - quadstealth.Seats += 0 -> new SeatDefinition() - quadstealth.Seats(0).Bailable = true + quadstealth.Seats += 0 -> bailableSeat quadstealth.CanCloak = true - quadstealth.MountPoints += 1 -> 0 - quadstealth.MountPoints += 2 -> 0 + quadstealth.MountPoints += 1 -> MountInfo(0) + quadstealth.MountPoints += 2 -> MountInfo(0) quadstealth.TrunkSize = InventoryTile.Tile1111 quadstealth.TrunkOffset = 30 quadstealth.TrunkLocation = Vector3(-1.71f, 0f, 0f) @@ -5695,11 +5717,12 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } quadstealth.DrownAtMaxDepth = true quadstealth.MaxDepth = 1.25f quadstealth.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + quadstealth.Geometry = atvForm two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser two_man_assault_buggy.MaxHealth = 1250 @@ -5707,14 +5730,12 @@ object GlobalDefinitions { two_man_assault_buggy.Repairable = true two_man_assault_buggy.RepairIfDestroyed = false two_man_assault_buggy.MaxShields = 250 - two_man_assault_buggy.Seats += 0 -> new SeatDefinition() - two_man_assault_buggy.Seats(0).Bailable = true - two_man_assault_buggy.Seats += 1 -> new SeatDefinition() - two_man_assault_buggy.Seats(1).Bailable = true - two_man_assault_buggy.Seats(1).ControlledWeapon = 2 + two_man_assault_buggy.Seats += 0 -> bailableSeat + two_man_assault_buggy.Seats += 1 -> bailableSeat + two_man_assault_buggy.controlledWeapons += 1 -> 2 two_man_assault_buggy.Weapons += 2 -> chaingun_p - two_man_assault_buggy.MountPoints += 1 -> 0 - two_man_assault_buggy.MountPoints += 2 -> 1 + two_man_assault_buggy.MountPoints += 1 -> MountInfo(0) + two_man_assault_buggy.MountPoints += 2 -> MountInfo(1) two_man_assault_buggy.TrunkSize = InventoryTile.Tile1511 two_man_assault_buggy.TrunkOffset = 30 two_man_assault_buggy.TrunkLocation = Vector3(-2.5f, 0f, 0f) @@ -5728,11 +5749,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } two_man_assault_buggy.DrownAtMaxDepth = true 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) skyguard.Name = "skyguard" skyguard.MaxHealth = 1000 @@ -5740,22 +5762,19 @@ object GlobalDefinitions { skyguard.Repairable = true skyguard.RepairIfDestroyed = false skyguard.MaxShields = 200 - skyguard.Seats += 0 -> new SeatDefinition() - skyguard.Seats(0).Bailable = true - skyguard.Seats += 1 -> new SeatDefinition() - skyguard.Seats(1).Bailable = true - skyguard.Seats(1).ControlledWeapon = 2 + skyguard.Seats += 0 -> bailableSeat + skyguard.Seats += 1 -> bailableSeat + skyguard.controlledWeapons += 1 -> 2 skyguard.Weapons += 2 -> skyguard_weapon_system - skyguard.MountPoints += 1 -> 0 - skyguard.MountPoints += 2 -> 0 - skyguard.MountPoints += 3 -> 1 + skyguard.MountPoints += 1 -> MountInfo(0) + skyguard.MountPoints += 2 -> MountInfo(0) + skyguard.MountPoints += 3 -> MountInfo(1) skyguard.TrunkSize = InventoryTile.Tile1511 skyguard.TrunkOffset = 30 skyguard.TrunkLocation = Vector3(2.5f, 0f, 0f) skyguard.AutoPilotSpeeds = (22, 8) skyguard.DestroyedModel = Some(DestroyedVehicle.Skyguard) skyguard.JackingDuration = Array(0, 15, 5, 3) - skyguard.explodes = true skyguard.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One @@ -5763,11 +5782,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } skyguard.DrownAtMaxDepth = true skyguard.MaxDepth = 1.5f skyguard.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + skyguard.Geometry = GeometryForm.representByCylinder(radius = 1.8867f, height = 1.4375f) threemanheavybuggy.Name = "threemanheavybuggy" // Marauder threemanheavybuggy.MaxHealth = 1700 @@ -5775,19 +5795,16 @@ object GlobalDefinitions { threemanheavybuggy.Repairable = true threemanheavybuggy.RepairIfDestroyed = false threemanheavybuggy.MaxShields = 340 - threemanheavybuggy.Seats += 0 -> new SeatDefinition() - threemanheavybuggy.Seats(0).Bailable = true - threemanheavybuggy.Seats += 1 -> new SeatDefinition() - threemanheavybuggy.Seats(1).Bailable = true - threemanheavybuggy.Seats(1).ControlledWeapon = 3 - threemanheavybuggy.Seats += 2 -> new SeatDefinition() - threemanheavybuggy.Seats(2).Bailable = true - threemanheavybuggy.Seats(2).ControlledWeapon = 4 + threemanheavybuggy.Seats += 0 -> bailableSeat + threemanheavybuggy.Seats += 1 -> bailableSeat + threemanheavybuggy.Seats += 2 -> bailableSeat + threemanheavybuggy.controlledWeapons += 1 -> 3 + threemanheavybuggy.controlledWeapons += 2 -> 4 threemanheavybuggy.Weapons += 3 -> chaingun_p threemanheavybuggy.Weapons += 4 -> grenade_launcher_marauder - threemanheavybuggy.MountPoints += 1 -> 0 - threemanheavybuggy.MountPoints += 2 -> 1 - threemanheavybuggy.MountPoints += 3 -> 2 + threemanheavybuggy.MountPoints += 1 -> MountInfo(0) + threemanheavybuggy.MountPoints += 2 -> MountInfo(1) + threemanheavybuggy.MountPoints += 3 -> MountInfo(2) threemanheavybuggy.TrunkSize = InventoryTile.Tile1511 threemanheavybuggy.TrunkOffset = 30 threemanheavybuggy.TrunkLocation = Vector3(3.01f, 0f, 0f) @@ -5802,11 +5819,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } threemanheavybuggy.DrownAtMaxDepth = true threemanheavybuggy.MaxDepth = 1.83f threemanheavybuggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + threemanheavybuggy.Geometry = GeometryForm.representByCylinder(radius = 2.1953f, height = 2.03125f) twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer twomanheavybuggy.MaxHealth = 1800 @@ -5814,14 +5832,12 @@ object GlobalDefinitions { twomanheavybuggy.Repairable = true twomanheavybuggy.RepairIfDestroyed = false twomanheavybuggy.MaxShields = 360 - twomanheavybuggy.Seats += 0 -> new SeatDefinition() - twomanheavybuggy.Seats(0).Bailable = true - twomanheavybuggy.Seats += 1 -> new SeatDefinition() - twomanheavybuggy.Seats(1).Bailable = true - twomanheavybuggy.Seats(1).ControlledWeapon = 2 + twomanheavybuggy.Seats += 0 -> bailableSeat + twomanheavybuggy.Seats += 1 -> bailableSeat + twomanheavybuggy.controlledWeapons += 1 -> 2 twomanheavybuggy.Weapons += 2 -> advanced_missile_launcher_t - twomanheavybuggy.MountPoints += 1 -> 0 - twomanheavybuggy.MountPoints += 2 -> 1 + twomanheavybuggy.MountPoints += 1 -> MountInfo(0) + twomanheavybuggy.MountPoints += 2 -> MountInfo(1) twomanheavybuggy.TrunkSize = InventoryTile.Tile1511 twomanheavybuggy.TrunkOffset = 30 twomanheavybuggy.TrunkLocation = Vector3(-0.23f, -2.05f, 0f) @@ -5836,11 +5852,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } twomanheavybuggy.DrownAtMaxDepth = true twomanheavybuggy.MaxDepth = 1.95f twomanheavybuggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + twomanheavybuggy.Geometry = GeometryForm.representByCylinder(radius = 2.60935f, height = 1.79688f) twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher twomanhoverbuggy.MaxHealth = 1600 @@ -5848,14 +5865,12 @@ object GlobalDefinitions { twomanhoverbuggy.Repairable = true twomanhoverbuggy.RepairIfDestroyed = false twomanhoverbuggy.MaxShields = 320 - twomanhoverbuggy.Seats += 0 -> new SeatDefinition() - twomanhoverbuggy.Seats(0).Bailable = true - twomanhoverbuggy.Seats += 1 -> new SeatDefinition() - twomanhoverbuggy.Seats(1).Bailable = true - twomanhoverbuggy.Seats(1).ControlledWeapon = 2 + twomanhoverbuggy.Seats += 0 -> bailableSeat + twomanhoverbuggy.Seats += 1 -> bailableSeat + twomanhoverbuggy.controlledWeapons += 1 -> 2 twomanhoverbuggy.Weapons += 2 -> flux_cannon_thresher - twomanhoverbuggy.MountPoints += 1 -> 0 - twomanhoverbuggy.MountPoints += 2 -> 1 + twomanhoverbuggy.MountPoints += 1 -> MountInfo(0) + twomanhoverbuggy.MountPoints += 2 -> MountInfo(1) twomanhoverbuggy.TrunkSize = InventoryTile.Tile1511 twomanhoverbuggy.TrunkOffset = 30 twomanhoverbuggy.TrunkLocation = Vector3(-3.39f, 0f, 0f) @@ -5870,10 +5885,11 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } twomanhoverbuggy.DrownAtMaxDepth = true twomanhoverbuggy.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the thresher hovers over water, so ...? + twomanhoverbuggy.Geometry = GeometryForm.representByCylinder(radius = 2.1875f, height = 2.01563f) mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.MaxHealth = 2500 @@ -5881,21 +5897,22 @@ object GlobalDefinitions { mediumtransport.Repairable = true mediumtransport.RepairIfDestroyed = false mediumtransport.MaxShields = 500 - mediumtransport.Seats += 0 -> new SeatDefinition() - mediumtransport.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + mediumtransport.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } mediumtransport.Seats += 1 -> new SeatDefinition() - mediumtransport.Seats(1).ControlledWeapon = 5 mediumtransport.Seats += 2 -> new SeatDefinition() - mediumtransport.Seats(2).ControlledWeapon = 6 - mediumtransport.Seats += 3 -> new SeatDefinition() - mediumtransport.Seats += 4 -> new SeatDefinition() - mediumtransport.Weapons += 5 -> mediumtransport_weapon_systemA - mediumtransport.Weapons += 6 -> mediumtransport_weapon_systemB - mediumtransport.MountPoints += 1 -> 0 - mediumtransport.MountPoints += 2 -> 1 - mediumtransport.MountPoints += 3 -> 2 - mediumtransport.MountPoints += 4 -> 3 - mediumtransport.MountPoints += 5 -> 4 + mediumtransport.Seats += 3 -> new SeatDefinition() + mediumtransport.Seats += 4 -> new SeatDefinition() + mediumtransport.controlledWeapons += 1 -> 5 + mediumtransport.controlledWeapons += 2 -> 6 + mediumtransport.Weapons += 5 -> mediumtransport_weapon_systemA + mediumtransport.Weapons += 6 -> mediumtransport_weapon_systemB + mediumtransport.MountPoints += 1 -> MountInfo(0) + mediumtransport.MountPoints += 2 -> MountInfo(1) + mediumtransport.MountPoints += 3 -> MountInfo(2) + mediumtransport.MountPoints += 4 -> MountInfo(3) + mediumtransport.MountPoints += 5 -> MountInfo(4) mediumtransport.TrunkSize = InventoryTile.Tile1515 mediumtransport.TrunkOffset = 30 mediumtransport.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -5910,11 +5927,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } mediumtransport.DrownAtMaxDepth = false mediumtransport.MaxDepth = 1.2f mediumtransport.UnderwaterLifespan(suffocation = -1, recovery = -1) + mediumtransport.Geometry = delivererForm battlewagon.Name = "battlewagon" // Raider battlewagon.MaxHealth = 2500 @@ -5922,25 +5940,26 @@ object GlobalDefinitions { battlewagon.Repairable = true battlewagon.RepairIfDestroyed = false battlewagon.MaxShields = 500 - battlewagon.Seats += 0 -> new SeatDefinition() - battlewagon.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + battlewagon.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } battlewagon.Seats += 1 -> new SeatDefinition() - battlewagon.Seats(1).ControlledWeapon = 5 battlewagon.Seats += 2 -> new SeatDefinition() - battlewagon.Seats(2).ControlledWeapon = 6 battlewagon.Seats += 3 -> new SeatDefinition() - battlewagon.Seats(3).ControlledWeapon = 7 battlewagon.Seats += 4 -> new SeatDefinition() - battlewagon.Seats(4).ControlledWeapon = 8 + battlewagon.controlledWeapons += 1 -> 5 + battlewagon.controlledWeapons += 2 -> 6 + battlewagon.controlledWeapons += 3 -> 7 + battlewagon.controlledWeapons += 4 -> 8 battlewagon.Weapons += 5 -> battlewagon_weapon_systema battlewagon.Weapons += 6 -> battlewagon_weapon_systemb battlewagon.Weapons += 7 -> battlewagon_weapon_systemc battlewagon.Weapons += 8 -> battlewagon_weapon_systemd - battlewagon.MountPoints += 1 -> 0 - battlewagon.MountPoints += 2 -> 1 - battlewagon.MountPoints += 3 -> 2 - battlewagon.MountPoints += 4 -> 3 - battlewagon.MountPoints += 5 -> 4 + battlewagon.MountPoints += 1 -> MountInfo(0) + battlewagon.MountPoints += 2 -> MountInfo(1) + battlewagon.MountPoints += 3 -> MountInfo(2) + battlewagon.MountPoints += 4 -> MountInfo(3) + battlewagon.MountPoints += 5 -> MountInfo(4) battlewagon.TrunkSize = InventoryTile.Tile1515 battlewagon.TrunkOffset = 30 battlewagon.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -5954,11 +5973,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } battlewagon.DrownAtMaxDepth = true battlewagon.MaxDepth = 1.2f battlewagon.UnderwaterLifespan(suffocation = -1, recovery = -1) + battlewagon.Geometry = delivererForm thunderer.Name = "thunderer" thunderer.MaxHealth = 2500 @@ -5966,21 +5986,22 @@ object GlobalDefinitions { thunderer.Repairable = true thunderer.RepairIfDestroyed = false thunderer.MaxShields = 500 - thunderer.Seats += 0 -> new SeatDefinition() - thunderer.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + thunderer.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } thunderer.Seats += 1 -> new SeatDefinition() - thunderer.Seats(1).ControlledWeapon = 5 thunderer.Seats += 2 -> new SeatDefinition() - thunderer.Seats(2).ControlledWeapon = 6 - thunderer.Seats += 3 -> new SeatDefinition() - thunderer.Seats += 4 -> new SeatDefinition() - thunderer.Weapons += 5 -> thunderer_weapon_systema - thunderer.Weapons += 6 -> thunderer_weapon_systemb - thunderer.MountPoints += 1 -> 0 - thunderer.MountPoints += 2 -> 1 - thunderer.MountPoints += 3 -> 2 - thunderer.MountPoints += 4 -> 3 - thunderer.MountPoints += 5 -> 4 + thunderer.Seats += 3 -> new SeatDefinition() + thunderer.Seats += 4 -> new SeatDefinition() + thunderer.Weapons += 5 -> thunderer_weapon_systema + thunderer.Weapons += 6 -> thunderer_weapon_systemb + thunderer.controlledWeapons += 1 -> 5 + thunderer.controlledWeapons += 2 -> 6 + thunderer.MountPoints += 1 -> MountInfo(0) + thunderer.MountPoints += 2 -> MountInfo(1) + thunderer.MountPoints += 3 -> MountInfo(2) + thunderer.MountPoints += 4 -> MountInfo(3) + thunderer.MountPoints += 5 -> MountInfo(4) thunderer.TrunkSize = InventoryTile.Tile1515 thunderer.TrunkOffset = 30 thunderer.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -5995,11 +6016,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } thunderer.DrownAtMaxDepth = true thunderer.MaxDepth = 1.2f thunderer.UnderwaterLifespan(suffocation = -1, recovery = -1) + thunderer.Geometry = delivererForm aurora.Name = "aurora" aurora.MaxHealth = 2500 @@ -6007,21 +6029,22 @@ object GlobalDefinitions { aurora.Repairable = true aurora.RepairIfDestroyed = false aurora.MaxShields = 500 - aurora.Seats += 0 -> new SeatDefinition() - aurora.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + aurora.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } aurora.Seats += 1 -> new SeatDefinition() - aurora.Seats(1).ControlledWeapon = 5 aurora.Seats += 2 -> new SeatDefinition() - aurora.Seats(2).ControlledWeapon = 6 - aurora.Seats += 3 -> new SeatDefinition() - aurora.Seats += 4 -> new SeatDefinition() - aurora.Weapons += 5 -> aurora_weapon_systema - aurora.Weapons += 6 -> aurora_weapon_systemb - aurora.MountPoints += 1 -> 0 - aurora.MountPoints += 2 -> 1 - aurora.MountPoints += 3 -> 2 - aurora.MountPoints += 4 -> 3 - aurora.MountPoints += 5 -> 4 + aurora.Seats += 3 -> new SeatDefinition() + aurora.Seats += 4 -> new SeatDefinition() + aurora.controlledWeapons += 1 -> 5 + aurora.controlledWeapons += 2 -> 6 + aurora.Weapons += 5 -> aurora_weapon_systema + aurora.Weapons += 6 -> aurora_weapon_systemb + aurora.MountPoints += 1 -> MountInfo(0) + aurora.MountPoints += 2 -> MountInfo(1) + aurora.MountPoints += 3 -> MountInfo(2) + aurora.MountPoints += 4 -> MountInfo(3) + aurora.MountPoints += 5 -> MountInfo(4) aurora.TrunkSize = InventoryTile.Tile1515 aurora.TrunkOffset = 30 aurora.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -6036,11 +6059,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } aurora.DrownAtMaxDepth = true aurora.MaxDepth = 1.2f aurora.UnderwaterLifespan(suffocation = -1, recovery = -1) + aurora.Geometry = delivererForm apc_tr.Name = "apc_tr" // Juggernaut apc_tr.MaxHealth = 6000 @@ -6048,44 +6072,41 @@ object GlobalDefinitions { apc_tr.Repairable = true apc_tr.RepairIfDestroyed = false apc_tr.MaxShields = 1200 - apc_tr.Seats += 0 -> new SeatDefinition() - apc_tr.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - apc_tr.Seats += 1 -> new SeatDefinition() - apc_tr.Seats(1).ControlledWeapon = 11 - apc_tr.Seats += 2 -> new SeatDefinition() - apc_tr.Seats(2).ControlledWeapon = 12 - apc_tr.Seats += 3 -> new SeatDefinition() - apc_tr.Seats += 4 -> new SeatDefinition() - apc_tr.Seats += 5 -> new SeatDefinition() - apc_tr.Seats(5).ControlledWeapon = 15 - apc_tr.Seats += 6 -> new SeatDefinition() - apc_tr.Seats(6).ControlledWeapon = 16 - apc_tr.Seats += 7 -> new SeatDefinition() - apc_tr.Seats(7).ControlledWeapon = 13 - apc_tr.Seats += 8 -> new SeatDefinition() - apc_tr.Seats(8).ControlledWeapon = 14 - apc_tr.Seats += 9 -> new SeatDefinition() - apc_tr.Seats(9).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_tr.Seats += 10 -> new SeatDefinition() - apc_tr.Seats(10).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_tr.Weapons += 11 -> apc_weapon_systemc_tr - apc_tr.Weapons += 12 -> apc_weapon_systemb - apc_tr.Weapons += 13 -> apc_weapon_systema - apc_tr.Weapons += 14 -> apc_weapon_systemd_tr - apc_tr.Weapons += 15 -> apc_ballgun_r - apc_tr.Weapons += 16 -> apc_ballgun_l - apc_tr.MountPoints += 1 -> 0 - apc_tr.MountPoints += 2 -> 0 - apc_tr.MountPoints += 3 -> 1 - apc_tr.MountPoints += 4 -> 2 - apc_tr.MountPoints += 5 -> 3 - apc_tr.MountPoints += 6 -> 4 - apc_tr.MountPoints += 7 -> 5 - apc_tr.MountPoints += 8 -> 6 - apc_tr.MountPoints += 9 -> 7 - apc_tr.MountPoints += 10 -> 8 - apc_tr.MountPoints += 11 -> 9 - apc_tr.MountPoints += 12 -> 10 + apc_tr.Seats += 0 -> new SeatDefinition() + apc_tr.Seats += 1 -> new SeatDefinition() + apc_tr.Seats += 2 -> new SeatDefinition() + apc_tr.Seats += 3 -> new SeatDefinition() + apc_tr.Seats += 4 -> new SeatDefinition() + apc_tr.Seats += 5 -> new SeatDefinition() + apc_tr.Seats += 6 -> new SeatDefinition() + apc_tr.Seats += 7 -> new SeatDefinition() + apc_tr.Seats += 8 -> new SeatDefinition() + apc_tr.Seats += 9 -> maxOnlySeat + apc_tr.Seats += 10 -> maxOnlySeat + apc_tr.controlledWeapons += 1 -> 11 + apc_tr.controlledWeapons += 2 -> 12 + apc_tr.controlledWeapons += 5 -> 15 + apc_tr.controlledWeapons += 6 -> 16 + apc_tr.controlledWeapons += 7 -> 13 + apc_tr.controlledWeapons += 8 -> 14 + apc_tr.Weapons += 11 -> apc_weapon_systemc_tr + apc_tr.Weapons += 12 -> apc_weapon_systemb + apc_tr.Weapons += 13 -> apc_weapon_systema + apc_tr.Weapons += 14 -> apc_weapon_systemd_tr + apc_tr.Weapons += 15 -> apc_ballgun_r + apc_tr.Weapons += 16 -> apc_ballgun_l + apc_tr.MountPoints += 1 -> MountInfo(0) + apc_tr.MountPoints += 2 -> MountInfo(0) + apc_tr.MountPoints += 3 -> MountInfo(1) + apc_tr.MountPoints += 4 -> MountInfo(2) + apc_tr.MountPoints += 5 -> MountInfo(3) + apc_tr.MountPoints += 6 -> MountInfo(4) + apc_tr.MountPoints += 7 -> MountInfo(5) + apc_tr.MountPoints += 8 -> MountInfo(6) + apc_tr.MountPoints += 9 -> MountInfo(7) + apc_tr.MountPoints += 10 -> MountInfo(8) + apc_tr.MountPoints += 11 -> MountInfo(9) + apc_tr.MountPoints += 12 -> MountInfo(10) apc_tr.TrunkSize = InventoryTile.Tile2016 apc_tr.TrunkOffset = 30 apc_tr.TrunkLocation = Vector3(-5.82f, 0f, 0f) @@ -6100,11 +6121,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } apc_tr.DrownAtMaxDepth = true apc_tr.MaxDepth = 3 apc_tr.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) + apc_tr.Geometry = apcForm apc_nc.Name = "apc_nc" // Vindicator apc_nc.MaxHealth = 6000 @@ -6112,44 +6134,41 @@ object GlobalDefinitions { apc_nc.Repairable = true apc_nc.RepairIfDestroyed = false apc_nc.MaxShields = 1200 - apc_nc.Seats += 0 -> new SeatDefinition() - apc_nc.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - apc_nc.Seats += 1 -> new SeatDefinition() - apc_nc.Seats(1).ControlledWeapon = 11 - apc_nc.Seats += 2 -> new SeatDefinition() - apc_nc.Seats(2).ControlledWeapon = 12 - apc_nc.Seats += 3 -> new SeatDefinition() - apc_nc.Seats += 4 -> new SeatDefinition() - apc_nc.Seats += 5 -> new SeatDefinition() - apc_nc.Seats(5).ControlledWeapon = 15 - apc_nc.Seats += 6 -> new SeatDefinition() - apc_nc.Seats(6).ControlledWeapon = 16 - apc_nc.Seats += 7 -> new SeatDefinition() - apc_nc.Seats(7).ControlledWeapon = 13 + apc_nc.Seats += 0 -> new SeatDefinition() + apc_nc.Seats += 1 -> new SeatDefinition() + apc_nc.Seats += 2 -> new SeatDefinition() + apc_nc.Seats += 3 -> new SeatDefinition() + apc_nc.Seats += 4 -> new SeatDefinition() + apc_nc.Seats += 5 -> new SeatDefinition() + apc_nc.Seats += 6 -> new SeatDefinition() + apc_nc.Seats += 7 -> new SeatDefinition() apc_nc.Seats += 8 -> new SeatDefinition() - apc_nc.Seats(8).ControlledWeapon = 14 - apc_nc.Seats += 9 -> new SeatDefinition() - apc_nc.Seats(9).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_nc.Seats += 10 -> new SeatDefinition() - apc_nc.Seats(10).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_nc.Weapons += 11 -> apc_weapon_systemc_nc - apc_nc.Weapons += 12 -> apc_weapon_systemb - apc_nc.Weapons += 13 -> apc_weapon_systema - apc_nc.Weapons += 14 -> apc_weapon_systemd_nc - apc_nc.Weapons += 15 -> apc_ballgun_r - apc_nc.Weapons += 16 -> apc_ballgun_l - apc_nc.MountPoints += 1 -> 0 - apc_nc.MountPoints += 2 -> 0 - apc_nc.MountPoints += 3 -> 1 - apc_nc.MountPoints += 4 -> 2 - apc_nc.MountPoints += 5 -> 3 - apc_nc.MountPoints += 6 -> 4 - apc_nc.MountPoints += 7 -> 5 - apc_nc.MountPoints += 8 -> 6 - apc_nc.MountPoints += 9 -> 7 - apc_nc.MountPoints += 10 -> 8 - apc_nc.MountPoints += 11 -> 9 - apc_nc.MountPoints += 12 -> 10 + apc_nc.Seats += 9 -> maxOnlySeat + apc_nc.Seats += 10 -> maxOnlySeat + apc_nc.controlledWeapons += 1 -> 11 + apc_nc.controlledWeapons += 2 -> 12 + apc_nc.controlledWeapons += 5 -> 15 + apc_nc.controlledWeapons += 6 -> 16 + apc_nc.controlledWeapons += 7 -> 13 + apc_nc.controlledWeapons += 8 -> 14 + apc_nc.Weapons += 11 -> apc_weapon_systemc_nc + apc_nc.Weapons += 12 -> apc_weapon_systemb + apc_nc.Weapons += 13 -> apc_weapon_systema + apc_nc.Weapons += 14 -> apc_weapon_systemd_nc + apc_nc.Weapons += 15 -> apc_ballgun_r + apc_nc.Weapons += 16 -> apc_ballgun_l + apc_nc.MountPoints += 1 -> MountInfo(0) + apc_nc.MountPoints += 2 -> MountInfo(0) + apc_nc.MountPoints += 3 -> MountInfo(1) + apc_nc.MountPoints += 4 -> MountInfo(2) + apc_nc.MountPoints += 5 -> MountInfo(3) + apc_nc.MountPoints += 6 -> MountInfo(4) + apc_nc.MountPoints += 7 -> MountInfo(5) + apc_nc.MountPoints += 8 -> MountInfo(6) + apc_nc.MountPoints += 9 -> MountInfo(7) + apc_nc.MountPoints += 10 -> MountInfo(8) + apc_nc.MountPoints += 11 -> MountInfo(9) + apc_nc.MountPoints += 12 -> MountInfo(10) apc_nc.TrunkSize = InventoryTile.Tile2016 apc_nc.TrunkOffset = 30 apc_nc.TrunkLocation = Vector3(-5.82f, 0f, 0f) @@ -6164,11 +6183,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } apc_nc.DrownAtMaxDepth = true apc_nc.MaxDepth = 3 apc_nc.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) + apc_nc.Geometry = apcForm apc_vs.Name = "apc_vs" // Leviathan apc_vs.MaxHealth = 6000 @@ -6176,44 +6196,41 @@ object GlobalDefinitions { apc_vs.Repairable = true apc_vs.RepairIfDestroyed = false apc_vs.MaxShields = 1200 - apc_vs.Seats += 0 -> new SeatDefinition() - apc_vs.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - apc_vs.Seats += 1 -> new SeatDefinition() - apc_vs.Seats(1).ControlledWeapon = 11 - apc_vs.Seats += 2 -> new SeatDefinition() - apc_vs.Seats(2).ControlledWeapon = 12 - apc_vs.Seats += 3 -> new SeatDefinition() - apc_vs.Seats += 4 -> new SeatDefinition() - apc_vs.Seats += 5 -> new SeatDefinition() - apc_vs.Seats(5).ControlledWeapon = 15 - apc_vs.Seats += 6 -> new SeatDefinition() - apc_vs.Seats(6).ControlledWeapon = 16 - apc_vs.Seats += 7 -> new SeatDefinition() - apc_vs.Seats(7).ControlledWeapon = 13 - apc_vs.Seats += 8 -> new SeatDefinition() - apc_vs.Seats(8).ControlledWeapon = 14 - apc_vs.Seats += 9 -> new SeatDefinition() - apc_vs.Seats(9).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_vs.Seats += 10 -> new SeatDefinition() - apc_vs.Seats(10).ArmorRestriction = SeatArmorRestriction.MaxOnly - apc_vs.Weapons += 11 -> apc_weapon_systemc_vs - apc_vs.Weapons += 12 -> apc_weapon_systemb - apc_vs.Weapons += 13 -> apc_weapon_systema - apc_vs.Weapons += 14 -> apc_weapon_systemd_vs - apc_vs.Weapons += 15 -> apc_ballgun_r - apc_vs.Weapons += 16 -> apc_ballgun_l - apc_vs.MountPoints += 1 -> 0 - apc_vs.MountPoints += 2 -> 0 - apc_vs.MountPoints += 3 -> 1 - apc_vs.MountPoints += 4 -> 2 - apc_vs.MountPoints += 5 -> 3 - apc_vs.MountPoints += 6 -> 4 - apc_vs.MountPoints += 7 -> 5 - apc_vs.MountPoints += 8 -> 6 - apc_vs.MountPoints += 9 -> 7 - apc_vs.MountPoints += 10 -> 8 - apc_vs.MountPoints += 11 -> 9 - apc_vs.MountPoints += 12 -> 10 + apc_vs.Seats += 0 -> new SeatDefinition() + apc_vs.Seats += 1 -> new SeatDefinition() + apc_vs.Seats += 2 -> new SeatDefinition() + apc_vs.Seats += 3 -> new SeatDefinition() + apc_vs.Seats += 4 -> new SeatDefinition() + apc_vs.Seats += 5 -> new SeatDefinition() + apc_vs.Seats += 6 -> new SeatDefinition() + apc_vs.Seats += 7 -> new SeatDefinition() + apc_vs.Seats += 8 -> new SeatDefinition() + apc_vs.Seats += 9 -> maxOnlySeat + apc_vs.Seats += 10 -> maxOnlySeat + apc_vs.controlledWeapons += 1 -> 11 + apc_vs.controlledWeapons += 2 -> 12 + apc_vs.controlledWeapons += 5 -> 15 + apc_vs.controlledWeapons += 6 -> 16 + apc_vs.controlledWeapons += 7 -> 13 + apc_vs.controlledWeapons += 8 -> 14 + apc_vs.Weapons += 11 -> apc_weapon_systemc_vs + apc_vs.Weapons += 12 -> apc_weapon_systemb + apc_vs.Weapons += 13 -> apc_weapon_systema + apc_vs.Weapons += 14 -> apc_weapon_systemd_vs + apc_vs.Weapons += 15 -> apc_ballgun_r + apc_vs.Weapons += 16 -> apc_ballgun_l + apc_vs.MountPoints += 1 -> MountInfo(0) + apc_vs.MountPoints += 2 -> MountInfo(0) + apc_vs.MountPoints += 3 -> MountInfo(1) + apc_vs.MountPoints += 4 -> MountInfo(2) + apc_vs.MountPoints += 5 -> MountInfo(3) + apc_vs.MountPoints += 6 -> MountInfo(4) + apc_vs.MountPoints += 7 -> MountInfo(5) + apc_vs.MountPoints += 8 -> MountInfo(6) + apc_vs.MountPoints += 9 -> MountInfo(7) + apc_vs.MountPoints += 10 -> MountInfo(8) + apc_vs.MountPoints += 11 -> MountInfo(9) + apc_vs.MountPoints += 12 -> MountInfo(10) apc_vs.TrunkSize = InventoryTile.Tile2016 apc_vs.TrunkOffset = 30 apc_vs.TrunkLocation = Vector3(-5.82f, 0f, 0f) @@ -6228,11 +6245,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } apc_vs.DrownAtMaxDepth = true apc_vs.MaxDepth = 3 apc_vs.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) + apc_vs.Geometry = apcForm lightning.Name = "lightning" lightning.MaxHealth = 2000 @@ -6240,12 +6258,13 @@ object GlobalDefinitions { lightning.Repairable = true lightning.RepairIfDestroyed = false lightning.MaxShields = 400 - lightning.Seats += 0 -> new SeatDefinition() - lightning.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - lightning.Seats(0).ControlledWeapon = 1 - lightning.Weapons += 1 -> lightning_weapon_system - lightning.MountPoints += 1 -> 0 - lightning.MountPoints += 2 -> 0 + lightning.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } + lightning.controlledWeapons += 0 -> 1 + lightning.Weapons += 1 -> lightning_weapon_system + lightning.MountPoints += 1 -> MountInfo(0) + lightning.MountPoints += 2 -> MountInfo(0) lightning.TrunkSize = InventoryTile.Tile1511 lightning.TrunkOffset = 30 lightning.TrunkLocation = Vector3(-3f, 0f, 0f) @@ -6260,11 +6279,12 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } lightning.DrownAtMaxDepth = true lightning.MaxDepth = 1.38f lightning.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) + lightning.Geometry = GeometryForm.representByCylinder(radius = 2.5078f, height = 1.79688f) prowler.Name = "prowler" prowler.MaxHealth = 4800 @@ -6272,17 +6292,18 @@ object GlobalDefinitions { prowler.Repairable = true prowler.RepairIfDestroyed = false prowler.MaxShields = 960 - prowler.Seats += 0 -> new SeatDefinition() - prowler.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + prowler.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } prowler.Seats += 1 -> new SeatDefinition() - prowler.Seats(1).ControlledWeapon = 3 prowler.Seats += 2 -> new SeatDefinition() - prowler.Seats(2).ControlledWeapon = 4 + prowler.controlledWeapons += 1 -> 3 + prowler.controlledWeapons += 2 -> 4 prowler.Weapons += 3 -> prowler_weapon_systemA prowler.Weapons += 4 -> prowler_weapon_systemB - prowler.MountPoints += 1 -> 0 - prowler.MountPoints += 2 -> 1 - prowler.MountPoints += 3 -> 2 + prowler.MountPoints += 1 -> MountInfo(0) + prowler.MountPoints += 2 -> MountInfo(1) + prowler.MountPoints += 3 -> MountInfo(2) prowler.TrunkSize = InventoryTile.Tile1511 prowler.TrunkOffset = 30 prowler.TrunkLocation = Vector3(-4.71f, 0f, 0f) @@ -6297,11 +6318,12 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } prowler.DrownAtMaxDepth = true prowler.MaxDepth = 3 prowler.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) + prowler.Geometry = GeometryForm.representByCylinder(radius = 3.461f, height = 3.48438f) vanguard.Name = "vanguard" vanguard.MaxHealth = 5400 @@ -6309,13 +6331,14 @@ object GlobalDefinitions { vanguard.Repairable = true vanguard.RepairIfDestroyed = false vanguard.MaxShields = 1080 - vanguard.Seats += 0 -> new SeatDefinition() - vanguard.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax + vanguard.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } vanguard.Seats += 1 -> new SeatDefinition() - vanguard.Seats(1).ControlledWeapon = 2 - vanguard.Weapons += 2 -> vanguard_weapon_system - vanguard.MountPoints += 1 -> 0 - vanguard.MountPoints += 2 -> 1 + vanguard.controlledWeapons += 1 -> 2 + vanguard.Weapons += 2 -> vanguard_weapon_system + vanguard.MountPoints += 1 -> MountInfo(0) + vanguard.MountPoints += 2 -> MountInfo(1) vanguard.TrunkSize = InventoryTile.Tile1511 vanguard.TrunkOffset = 30 vanguard.TrunkLocation = Vector3(-4.84f, 0f, 0f) @@ -6330,11 +6353,12 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } vanguard.DrownAtMaxDepth = true vanguard.MaxDepth = 2.7f vanguard.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) + vanguard.Geometry = GeometryForm.representByCylinder(radius = 3.8554f, height = 2.60938f) magrider.Name = "magrider" magrider.MaxHealth = 4200 @@ -6342,15 +6366,16 @@ object GlobalDefinitions { magrider.Repairable = true magrider.RepairIfDestroyed = false magrider.MaxShields = 840 - magrider.Seats += 0 -> new SeatDefinition() - magrider.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - magrider.Seats(0).ControlledWeapon = 2 + magrider.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } magrider.Seats += 1 -> new SeatDefinition() - magrider.Seats(1).ControlledWeapon = 3 - magrider.Weapons += 2 -> particle_beam_magrider - magrider.Weapons += 3 -> heavy_rail_beam_magrider - magrider.MountPoints += 1 -> 0 - magrider.MountPoints += 2 -> 1 + magrider.controlledWeapons += 0 -> 2 + magrider.controlledWeapons += 1 -> 3 + magrider.Weapons += 2 -> particle_beam_magrider + magrider.Weapons += 3 -> heavy_rail_beam_magrider + magrider.MountPoints += 1 -> MountInfo(0) + magrider.MountPoints += 2 -> MountInfo(1) magrider.TrunkSize = InventoryTile.Tile1511 magrider.TrunkOffset = 30 magrider.TrunkLocation = Vector3(5.06f, 0f, 0f) @@ -6365,11 +6390,12 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } magrider.DrownAtMaxDepth = true 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) val utilityConverter = new UtilityVehicleConverter ant.Name = "ant" @@ -6378,10 +6404,11 @@ object GlobalDefinitions { ant.Repairable = true ant.RepairIfDestroyed = false ant.MaxShields = 400 - ant.Seats += 0 -> new SeatDefinition() - ant.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - ant.MountPoints += 1 -> 0 - ant.MountPoints += 2 -> 0 + ant.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } + ant.MountPoints += 1 -> MountInfo(0) + ant.MountPoints += 2 -> MountInfo(0) ant.Deployment = true ant.DeployTime = 1500 ant.UndeployTime = 1500 @@ -6398,11 +6425,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } ant.DrownAtMaxDepth = true ant.MaxDepth = 2 ant.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) + ant.Geometry = GeometryForm.representByCylinder(radius = 2.16795f, height = 2.09376f) //TODO hexahedron ams.Name = "ams" ams.MaxHealth = 5000 // Temporary - original value is 3000 @@ -6410,10 +6438,11 @@ object GlobalDefinitions { ams.Repairable = true ams.RepairIfDestroyed = false ams.MaxShields = 1000 // Temporary - original value is 600 + 1 - ams.Seats += 0 -> new SeatDefinition() - ams.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax - ams.MountPoints += 1 -> 0 - ams.MountPoints += 2 -> 0 + ams.Seats += 0 -> new SeatDefinition() { + restriction = NoReinforcedOrMax + } + ams.MountPoints += 1 -> MountInfo(0) + ams.MountPoints += 2 -> MountInfo(0) ams.Utilities += 1 -> UtilityType.matrix_terminalc ams.Utilities += 2 -> UtilityType.ams_respawn_tube ams.Utilities += 3 -> UtilityType.order_terminala @@ -6434,11 +6463,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } ams.DrownAtMaxDepth = true ams.MaxDepth = 3 ams.UnderwaterLifespan(suffocation = 5000L, recovery = 5000L) + ams.Geometry = GeometryForm.representByCylinder(radius = 3.0117f, height = 3.39062f) //TODO hexahedron val variantConverter = new VariantVehicleConverter router.Name = "router" @@ -6448,7 +6478,7 @@ object GlobalDefinitions { router.RepairIfDestroyed = false router.MaxShields = 800 router.Seats += 0 -> new SeatDefinition() - router.MountPoints += 1 -> 0 + router.MountPoints += 1 -> MountInfo(0) router.Utilities += 1 -> UtilityType.teleportpad_terminal router.Utilities += 2 -> UtilityType.internal_router_telepad_deployable router.TrunkSize = InventoryTile.Tile1511 @@ -6470,11 +6500,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } router.DrownAtMaxDepth = true router.MaxDepth = 2 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 switchblade.Name = "switchblade" switchblade.MaxHealth = 1750 @@ -6483,10 +6514,10 @@ object GlobalDefinitions { switchblade.RepairIfDestroyed = false switchblade.MaxShields = 350 switchblade.Seats += 0 -> new SeatDefinition() - switchblade.Seats(0).ControlledWeapon = 1 + switchblade.controlledWeapons += 0 -> 1 switchblade.Weapons += 1 -> scythe - switchblade.MountPoints += 1 -> 0 - switchblade.MountPoints += 2 -> 0 + switchblade.MountPoints += 1 -> MountInfo(0) + switchblade.MountPoints += 2 -> MountInfo(0) switchblade.TrunkSize = InventoryTile.Tile1511 switchblade.TrunkOffset = 30 switchblade.TrunkLocation = Vector3(-2.5f, 0f, 0f) @@ -6506,11 +6537,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } switchblade.DrownAtMaxDepth = true switchblade.MaxDepth = 2 switchblade.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the switchblade hovers over water, so ...? + switchblade.Geometry = GeometryForm.representByCylinder(radius = 2.4335f, height = 2.73438f) flail.Name = "flail" flail.MaxHealth = 2400 @@ -6519,9 +6551,9 @@ object GlobalDefinitions { flail.RepairIfDestroyed = false flail.MaxShields = 480 flail.Seats += 0 -> new SeatDefinition() - flail.Seats(0).ControlledWeapon = 1 - flail.Weapons += 1 -> flail_weapon - flail.MountPoints += 1 -> 0 + flail.controlledWeapons += 0 -> 1 + flail.Weapons += 1 -> flail_weapon + flail.MountPoints += 1 -> MountInfo(0) flail.TrunkSize = InventoryTile.Tile1511 flail.TrunkOffset = 30 flail.TrunkLocation = Vector3(-3.75f, 0f, 0f) @@ -6540,11 +6572,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } flail.DrownAtMaxDepth = true 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) mosquito.Name = "mosquito" mosquito.MaxHealth = 665 @@ -6553,12 +6586,11 @@ object GlobalDefinitions { mosquito.RepairIfDestroyed = false mosquito.MaxShields = 133 mosquito.CanFly = true - mosquito.Seats += 0 -> new SeatDefinition() - mosquito.Seats(0).Bailable = true - mosquito.Seats(0).ControlledWeapon = 1 + mosquito.Seats += 0 -> bailableSeat + mosquito.controlledWeapons += 0 -> 1 mosquito.Weapons += 1 -> rotarychaingun_mosquito - mosquito.MountPoints += 1 -> 0 - mosquito.MountPoints += 2 -> 0 + mosquito.MountPoints += 1 -> MountInfo(0) + mosquito.MountPoints += 2 -> MountInfo(0) mosquito.TrunkSize = InventoryTile.Tile1111 mosquito.TrunkOffset = 30 mosquito.TrunkLocation = Vector3(-4.6f, 0f, 0f) @@ -6574,10 +6606,11 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } mosquito.DrownAtMaxDepth = true mosquito.MaxDepth = 2 //flying vehicles will automatically disable + mosquito.Geometry = GeometryForm.representByCylinder(radius = 2.72108f, height = 2.5f) lightgunship.Name = "lightgunship" // Reaver lightgunship.MaxHealth = 1000 @@ -6586,12 +6619,11 @@ object GlobalDefinitions { lightgunship.RepairIfDestroyed = false lightgunship.MaxShields = 200 lightgunship.CanFly = true - lightgunship.Seats += 0 -> new SeatDefinition() - lightgunship.Seats(0).Bailable = true - lightgunship.Seats(0).ControlledWeapon = 1 + lightgunship.Seats += 0 -> bailableSeat + lightgunship.controlledWeapons += 0 -> 1 lightgunship.Weapons += 1 -> lightgunship_weapon_system - lightgunship.MountPoints += 1 -> 0 - lightgunship.MountPoints += 2 -> 0 + lightgunship.MountPoints += 1 -> MountInfo(0) + lightgunship.MountPoints += 2 -> MountInfo(0) lightgunship.TrunkSize = InventoryTile.Tile1511 lightgunship.TrunkOffset = 30 lightgunship.TrunkLocation = Vector3(-5.61f, 0f, 0f) @@ -6608,10 +6640,11 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } lightgunship.DrownAtMaxDepth = true lightgunship.MaxDepth = 2 //flying vehicles will automatically disable + lightgunship.Geometry = GeometryForm.representByCylinder(radius = 2.375f, height = 1.98438f) wasp.Name = "wasp" wasp.MaxHealth = 515 @@ -6620,12 +6653,11 @@ object GlobalDefinitions { wasp.RepairIfDestroyed = false wasp.MaxShields = 103 wasp.CanFly = true - wasp.Seats += 0 -> new SeatDefinition() - wasp.Seats(0).Bailable = true - wasp.Seats(0).ControlledWeapon = 1 + wasp.Seats += 0 -> bailableSeat + wasp.controlledWeapons += 0 -> 1 wasp.Weapons += 1 -> wasp_weapon_system - wasp.MountPoints += 1 -> 0 - wasp.MountPoints += 2 -> 0 + wasp.MountPoints += 1 -> MountInfo(0) + wasp.MountPoints += 2 -> MountInfo(0) wasp.TrunkSize = InventoryTile.Tile1111 wasp.TrunkOffset = 30 wasp.TrunkLocation = Vector3(-4.6f, 0f, 0f) @@ -6641,10 +6673,11 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } wasp.DrownAtMaxDepth = true wasp.MaxDepth = 2 //flying vehicles will automatically disable + wasp.Geometry = GeometryForm.representByCylinder(radius = 2.88675f, height = 2.5f) liberator.Name = "liberator" liberator.MaxHealth = 2500 @@ -6654,18 +6687,18 @@ object GlobalDefinitions { liberator.MaxShields = 500 liberator.CanFly = true liberator.Seats += 0 -> new SeatDefinition() - liberator.Seats(0).ControlledWeapon = 3 - liberator.Seats += 1 -> new SeatDefinition() - liberator.Seats(1).ControlledWeapon = 4 - liberator.Seats += 2 -> new SeatDefinition() - liberator.Seats(2).ControlledWeapon = 5 + liberator.Seats += 1 -> bailableSeat + liberator.Seats += 2 -> bailableSeat + liberator.controlledWeapons += 0 -> 3 + liberator.controlledWeapons += 1 -> 4 + liberator.controlledWeapons += 2 -> 5 liberator.Weapons += 3 -> liberator_weapon_system liberator.Weapons += 4 -> liberator_bomb_bay liberator.Weapons += 5 -> liberator_25mm_cannon - liberator.MountPoints += 1 -> 0 - liberator.MountPoints += 2 -> 1 - liberator.MountPoints += 3 -> 1 - liberator.MountPoints += 4 -> 2 + liberator.MountPoints += 1 -> MountInfo(0) + liberator.MountPoints += 2 -> MountInfo(1) + liberator.MountPoints += 3 -> MountInfo(1) + liberator.MountPoints += 4 -> MountInfo(2) liberator.TrunkSize = InventoryTile.Tile1515 liberator.TrunkOffset = 30 liberator.TrunkLocation = Vector3(-0.76f, -1.88f, 0f) @@ -6682,10 +6715,11 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } liberator.DrownAtMaxDepth = true liberator.MaxDepth = 2 //flying vehicles will automatically disable + liberator.Geometry = liberatorForm vulture.Name = "vulture" vulture.MaxHealth = 2500 @@ -6695,18 +6729,18 @@ object GlobalDefinitions { vulture.MaxShields = 500 vulture.CanFly = true vulture.Seats += 0 -> new SeatDefinition() - vulture.Seats(0).ControlledWeapon = 3 - vulture.Seats += 1 -> new SeatDefinition() - vulture.Seats(1).ControlledWeapon = 4 - vulture.Seats += 2 -> new SeatDefinition() - vulture.Seats(2).ControlledWeapon = 5 + vulture.Seats += 1 -> bailableSeat + vulture.Seats += 2 -> bailableSeat + vulture.controlledWeapons += 0 -> 3 + vulture.controlledWeapons += 1 -> 4 + vulture.controlledWeapons += 2 -> 5 vulture.Weapons += 3 -> vulture_nose_weapon_system vulture.Weapons += 4 -> vulture_bomb_bay vulture.Weapons += 5 -> vulture_tail_cannon - vulture.MountPoints += 1 -> 0 - vulture.MountPoints += 2 -> 1 - vulture.MountPoints += 3 -> 1 - vulture.MountPoints += 4 -> 2 + vulture.MountPoints += 1 -> MountInfo(0) + vulture.MountPoints += 2 -> MountInfo(1) + vulture.MountPoints += 3 -> MountInfo(1) + vulture.MountPoints += 4 -> MountInfo(2) vulture.TrunkSize = InventoryTile.Tile1611 vulture.TrunkOffset = 30 vulture.TrunkLocation = Vector3(-0.76f, -1.88f, 0f) @@ -6724,10 +6758,11 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } vulture.DrownAtMaxDepth = true vulture.MaxDepth = 2 //flying vehicles will automatically disable + vulture.Geometry = liberatorForm dropship.Name = "dropship" // Galaxy dropship.MaxHealth = 5000 @@ -6738,50 +6773,46 @@ object GlobalDefinitions { dropship.MaxShields = 1000 dropship.CanFly = true dropship.Seats += 0 -> new SeatDefinition() - dropship.Seats += 1 -> new SeatDefinition() - dropship.Seats(1).Bailable = true - dropship.Seats(1).ControlledWeapon = 12 - dropship.Seats += 2 -> new SeatDefinition() - dropship.Seats(2).Bailable = true - dropship.Seats(2).ControlledWeapon = 13 - dropship.Seats += 3 -> new SeatDefinition() - dropship.Seats(3).Bailable = true - dropship.Seats += 4 -> new SeatDefinition() - dropship.Seats(4).Bailable = true - dropship.Seats += 5 -> new SeatDefinition() - dropship.Seats(5).Bailable = true - dropship.Seats += 6 -> new SeatDefinition() - dropship.Seats(6).Bailable = true - dropship.Seats += 7 -> new SeatDefinition() - dropship.Seats(7).Bailable = true - dropship.Seats += 8 -> new SeatDefinition() - dropship.Seats(8).Bailable = true - dropship.Seats += 9 -> new SeatDefinition() - dropship.Seats(9).Bailable = true - dropship.Seats(9).ArmorRestriction = SeatArmorRestriction.MaxOnly - dropship.Seats += 10 -> new SeatDefinition() - dropship.Seats(10).Bailable = true - dropship.Seats(10).ArmorRestriction = SeatArmorRestriction.MaxOnly - dropship.Seats += 11 -> new SeatDefinition() - dropship.Seats(11).Bailable = true - dropship.Seats(11).ControlledWeapon = 14 + dropship.Seats += 1 -> new SeatDefinition() { + bailable = true + } + dropship.Seats += 2 -> bailableSeat + dropship.Seats += 3 -> bailableSeat + dropship.Seats += 4 -> bailableSeat + dropship.Seats += 5 -> bailableSeat + dropship.Seats += 6 -> bailableSeat + dropship.Seats += 7 -> bailableSeat + dropship.Seats += 9 -> new SeatDefinition() { + bailable = true + restriction = MaxOnly + } + dropship.Seats += 10 -> new SeatDefinition() { + bailable = true + restriction = MaxOnly + } + dropship.Seats += 11 -> bailableSeat + dropship.controlledWeapons += 1 -> 12 + dropship.controlledWeapons += 2 -> 13 + dropship.controlledWeapons += 11 -> 14 dropship.Weapons += 12 -> cannon_dropship_20mm dropship.Weapons += 13 -> cannon_dropship_20mm dropship.Weapons += 14 -> dropship_rear_turret - dropship.Cargo += 15 -> new CargoDefinition() - dropship.MountPoints += 1 -> 0 - dropship.MountPoints += 2 -> 11 - dropship.MountPoints += 3 -> 1 - dropship.MountPoints += 4 -> 2 - dropship.MountPoints += 5 -> 3 - dropship.MountPoints += 6 -> 4 - dropship.MountPoints += 7 -> 5 - dropship.MountPoints += 8 -> 6 - dropship.MountPoints += 9 -> 7 - dropship.MountPoints += 10 -> 8 - dropship.MountPoints += 11 -> 9 - dropship.MountPoints += 12 -> 10 - dropship.MountPoints += 13 -> 15 + dropship.Cargo += 15 -> new CargoDefinition() { + restriction = SmallCargo + } + dropship.MountPoints += 1 -> MountInfo(0) + dropship.MountPoints += 2 -> MountInfo(11) + dropship.MountPoints += 3 -> MountInfo(1) + dropship.MountPoints += 4 -> MountInfo(2) + dropship.MountPoints += 5 -> MountInfo(3) + dropship.MountPoints += 6 -> MountInfo(4) + dropship.MountPoints += 7 -> MountInfo(5) + dropship.MountPoints += 8 -> MountInfo(6) + dropship.MountPoints += 9 -> MountInfo(7) + dropship.MountPoints += 10 -> MountInfo(8) + dropship.MountPoints += 11 -> MountInfo(9) + dropship.MountPoints += 12 -> MountInfo(10) + dropship.MountPoints += 13 -> MountInfo(15) dropship.TrunkSize = InventoryTile.Tile1612 dropship.TrunkOffset = 30 dropship.TrunkLocation = Vector3(-7.39f, -4.96f, 0f) @@ -6798,10 +6829,11 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } dropship.DrownAtMaxDepth = true dropship.MaxDepth = 2 + dropship.Geometry = GeometryForm.representByCylinder(radius = 10.52202f, height = 6.23438f) galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.MaxHealth = 6000 @@ -6812,27 +6844,27 @@ object GlobalDefinitions { galaxy_gunship.MaxShields = 1200 galaxy_gunship.CanFly = true galaxy_gunship.Seats += 0 -> new SeatDefinition() - galaxy_gunship.Seats += 1 -> new SeatDefinition() - galaxy_gunship.Seats(1).ControlledWeapon = 6 - galaxy_gunship.Seats += 2 -> new SeatDefinition() - galaxy_gunship.Seats(2).ControlledWeapon = 7 - galaxy_gunship.Seats += 3 -> new SeatDefinition() - galaxy_gunship.Seats(3).ControlledWeapon = 8 - galaxy_gunship.Seats += 4 -> new SeatDefinition() - galaxy_gunship.Seats(4).ControlledWeapon = 9 - galaxy_gunship.Seats += 5 -> new SeatDefinition() - galaxy_gunship.Seats(5).ControlledWeapon = 10 + galaxy_gunship.Seats += 1 -> bailableSeat + galaxy_gunship.Seats += 2 -> bailableSeat + galaxy_gunship.Seats += 3 -> bailableSeat + galaxy_gunship.Seats += 4 -> bailableSeat + galaxy_gunship.Seats += 5 -> bailableSeat + galaxy_gunship.controlledWeapons += 1 -> 6 + galaxy_gunship.controlledWeapons += 2 -> 7 + galaxy_gunship.controlledWeapons += 3 -> 8 + galaxy_gunship.controlledWeapons += 4 -> 9 + galaxy_gunship.controlledWeapons += 5 -> 10 galaxy_gunship.Weapons += 6 -> galaxy_gunship_cannon galaxy_gunship.Weapons += 7 -> galaxy_gunship_cannon galaxy_gunship.Weapons += 8 -> galaxy_gunship_tailgun galaxy_gunship.Weapons += 9 -> galaxy_gunship_gun galaxy_gunship.Weapons += 10 -> galaxy_gunship_gun - galaxy_gunship.MountPoints += 1 -> 0 - galaxy_gunship.MountPoints += 2 -> 3 - galaxy_gunship.MountPoints += 3 -> 1 - galaxy_gunship.MountPoints += 4 -> 2 - galaxy_gunship.MountPoints += 5 -> 4 - galaxy_gunship.MountPoints += 6 -> 5 + galaxy_gunship.MountPoints += 1 -> MountInfo(0) + galaxy_gunship.MountPoints += 2 -> MountInfo(3) + galaxy_gunship.MountPoints += 3 -> MountInfo(1) + galaxy_gunship.MountPoints += 4 -> MountInfo(2) + galaxy_gunship.MountPoints += 5 -> MountInfo(4) + galaxy_gunship.MountPoints += 6 -> MountInfo(5) galaxy_gunship.TrunkSize = InventoryTile.Tile1816 galaxy_gunship.TrunkOffset = 30 galaxy_gunship.TrunkLocation = Vector3(-9.85f, 0f, 0f) @@ -6851,10 +6883,11 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } galaxy_gunship.DrownAtMaxDepth = true galaxy_gunship.MaxDepth = 2 + galaxy_gunship.Geometry = GeometryForm.representByCylinder(radius = 9.2382f, height = 5.01562f) lodestar.Name = "lodestar" lodestar.MaxHealth = 5000 @@ -6865,8 +6898,8 @@ object GlobalDefinitions { lodestar.MaxShields = 1000 lodestar.CanFly = true lodestar.Seats += 0 -> new SeatDefinition() - lodestar.MountPoints += 1 -> 0 - lodestar.MountPoints += 2 -> 1 + lodestar.MountPoints += 1 -> MountInfo(0) + lodestar.MountPoints += 2 -> MountInfo(1) lodestar.Cargo += 1 -> new CargoDefinition() lodestar.Utilities += 2 -> UtilityType.lodestar_repair_terminal lodestar.UtilityOffset += 2 -> Vector3(0, 20, 0) @@ -6892,10 +6925,11 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } lodestar.DrownAtMaxDepth = true lodestar.MaxDepth = 2 + lodestar.Geometry = GeometryForm.representByCylinder(radius = 7.8671f, height = 6.79688f) //TODO hexahedron phantasm.Name = "phantasm" phantasm.MaxHealth = 2500 @@ -6906,19 +6940,15 @@ object GlobalDefinitions { phantasm.CanCloak = true phantasm.CanFly = true phantasm.Seats += 0 -> new SeatDefinition() - phantasm.Seats += 1 -> new SeatDefinition() - phantasm.Seats(1).Bailable = true - phantasm.Seats += 2 -> new SeatDefinition() - phantasm.Seats(2).Bailable = true - phantasm.Seats += 3 -> new SeatDefinition() - phantasm.Seats(3).Bailable = true - phantasm.Seats += 4 -> new SeatDefinition() - phantasm.Seats(4).Bailable = true - phantasm.MountPoints += 1 -> 0 - phantasm.MountPoints += 2 -> 1 - phantasm.MountPoints += 3 -> 2 - phantasm.MountPoints += 4 -> 3 - phantasm.MountPoints += 5 -> 4 + phantasm.Seats += 1 -> bailableSeat + phantasm.Seats += 2 -> bailableSeat + phantasm.Seats += 3 -> bailableSeat + phantasm.Seats += 4 -> bailableSeat + phantasm.MountPoints += 1 -> MountInfo(0) + phantasm.MountPoints += 2 -> MountInfo(1) + phantasm.MountPoints += 3 -> MountInfo(2) + phantasm.MountPoints += 4 -> MountInfo(3) + phantasm.MountPoints += 5 -> MountInfo(4) phantasm.TrunkSize = InventoryTile.Tile1107 phantasm.TrunkOffset = 30 phantasm.TrunkLocation = Vector3(-6.16f, 0f, 0f) @@ -6934,29 +6964,70 @@ object GlobalDefinitions { Damage1 = 150 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } phantasm.DrownAtMaxDepth = true phantasm.MaxDepth = 2 + phantasm.Geometry = GeometryForm.representByCylinder(radius = 5.2618f, height = 3f) droppod.Name = "droppod" droppod.MaxHealth = 20000 - //droppod.Damageable = false + droppod.Damageable = false + droppod.Repairable = false droppod.CanFly = true - droppod.Seats += 0 -> new SeatDefinition - droppod.MountPoints += 1 -> 0 + droppod.Seats += 0 -> new SeatDefinition { + restriction = Unrestricted + } + droppod.MountPoints += 1 -> MountInfo(0) droppod.TrunkSize = InventoryTile.None droppod.Packet = new DroppodConverter() droppod.DeconstructionTime = Some(5 seconds) droppod.DestroyedModel = None //the adb calls out a droppod; the cyclic nature of this confounds me droppod.DamageUsing = DamageCalculations.AgainstAircraft droppod.DrownAtMaxDepth = false + + orbital_shuttle.Name = "orbital_shuttle" + orbital_shuttle.MaxHealth = 20000 + orbital_shuttle.Damageable = false + orbital_shuttle.Repairable = false + orbital_shuttle.CanFly = true + orbital_shuttle.CanBeOwned = None + orbital_shuttle.undergoesDecay = false + orbital_shuttle.Seats += 0 -> new SeatDefinition { + occupancy = 300 + restriction = Unrestricted + } + /* + these are close to the mount point offsets in the ADB; + physically, they correlate to positions in the HART building rather than with the shuttle model by itself; + set the shuttle pad based on the zonemap extraction values then position the shuttle relative to that pad; + rotation based on the shuttle should place these offsets in the HART lobby whose gantry hall corresponds to that mount index + */ + orbital_shuttle.MountPoints += 1 -> MountInfo(0, Vector3(-62, 4, -28.2f)) + orbital_shuttle.MountPoints += 2 -> MountInfo(0, Vector3(-62, 28, -28.2f)) + orbital_shuttle.MountPoints += 3 -> MountInfo(0, Vector3(-62, 4, -18.2f)) + orbital_shuttle.MountPoints += 4 -> MountInfo(0, Vector3(-62, 28, -18.2f)) + orbital_shuttle.MountPoints += 5 -> MountInfo(0, Vector3( 62, 4, -28.2f)) + orbital_shuttle.MountPoints += 6 -> MountInfo(0, Vector3( 62, 28, -28.2f)) + orbital_shuttle.MountPoints += 7 -> MountInfo(0, Vector3( 62, 4, -18.2f)) + orbital_shuttle.MountPoints += 8 -> MountInfo(0, Vector3( 62, 28, -18.2f)) + orbital_shuttle.TrunkSize = InventoryTile.None + orbital_shuttle.Packet = new OrbitalShuttleConverter + orbital_shuttle.DeconstructionTime = None + orbital_shuttle.DestroyedModel = None + orbital_shuttle.DamageUsing = DamageCalculations.AgainstNothing + orbital_shuttle.DrownAtMaxDepth = false } /** * Initialize `Deployable` globals. */ private def init_deployables(): Unit = { + val mine = GeometryForm.representByCylinder(radius = 0.1914f, height = 0.0957f) _ + val smallTurret = GeometryForm.representByCylinder(radius = 0.48435f, height = 1.23438f) _ + val sensor = GeometryForm.representByCylinder(radius = 0.1914f, height = 1.21875f) _ + val largeTurret = GeometryForm.representByCylinder(radius = 0.8437f, height = 2.29687f) _ + boomer.Name = "boomer" boomer.Descriptor = "Boomers" boomer.MaxHealth = 100 @@ -6976,8 +7047,9 @@ object GlobalDefinitions { Damage4 = 1850 DamageRadius = 5.1f DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + boomer.Geometry = mine he_mine.Name = "he_mine" he_mine.Descriptor = "Mines" @@ -6997,8 +7069,9 @@ object GlobalDefinitions { Damage4 = 1600 DamageRadius = 6.6f DamageAtEdge = 0.25f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + he_mine.Geometry = mine jammer_mine.Name = "jammer_mine" jammer_mine.Descriptor = "JammerMines" @@ -7008,6 +7081,7 @@ object GlobalDefinitions { jammer_mine.Repairable = false jammer_mine.DeployTime = Duration.create(1000, "ms") jammer_mine.DetonateOnJamming = false + jammer_mine.Geometry = mine spitfire_turret.Name = "spitfire_turret" spitfire_turret.Descriptor = "Spitfires" @@ -7015,8 +7089,8 @@ object GlobalDefinitions { spitfire_turret.Damageable = true spitfire_turret.Repairable = true spitfire_turret.RepairIfDestroyed = false - spitfire_turret.Weapons += 1 -> new mutable.HashMap() - spitfire_turret.Weapons(1) += TurretUpgrade.None -> spitfire_weapon + spitfire_turret.WeaponPaths += 1 -> new mutable.HashMap() + spitfire_turret.WeaponPaths(1) += TurretUpgrade.None -> spitfire_weapon spitfire_turret.ReserveAmmunition = false spitfire_turret.DeployCategory = DeployableCategory.SmallTurrets spitfire_turret.DeployTime = Duration.create(5000, "ms") @@ -7029,8 +7103,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + spitfire_turret.Geometry = smallTurret spitfire_cloaked.Name = "spitfire_cloaked" spitfire_cloaked.Descriptor = "CloakingSpitfires" @@ -7038,8 +7113,8 @@ object GlobalDefinitions { spitfire_cloaked.Damageable = true spitfire_cloaked.Repairable = true spitfire_cloaked.RepairIfDestroyed = false - spitfire_cloaked.Weapons += 1 -> new mutable.HashMap() - spitfire_cloaked.Weapons(1) += TurretUpgrade.None -> spitfire_weapon + spitfire_cloaked.WeaponPaths += 1 -> new mutable.HashMap() + spitfire_cloaked.WeaponPaths(1) += TurretUpgrade.None -> spitfire_weapon spitfire_cloaked.ReserveAmmunition = false spitfire_cloaked.DeployCategory = DeployableCategory.SmallTurrets spitfire_cloaked.DeployTime = Duration.create(5000, "ms") @@ -7051,8 +7126,9 @@ object GlobalDefinitions { Damage1 = 75 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + spitfire_cloaked.Geometry = smallTurret spitfire_aa.Name = "spitfire_aa" spitfire_aa.Descriptor = "FlakSpitfires" @@ -7060,8 +7136,8 @@ object GlobalDefinitions { spitfire_aa.Damageable = true spitfire_aa.Repairable = true spitfire_aa.RepairIfDestroyed = false - spitfire_aa.Weapons += 1 -> new mutable.HashMap() - spitfire_aa.Weapons(1) += TurretUpgrade.None -> spitfire_aa_weapon + spitfire_aa.WeaponPaths += 1 -> new mutable.HashMap() + spitfire_aa.WeaponPaths(1) += TurretUpgrade.None -> spitfire_aa_weapon spitfire_aa.ReserveAmmunition = false spitfire_aa.DeployCategory = DeployableCategory.SmallTurrets spitfire_aa.DeployTime = Duration.create(5000, "ms") @@ -7073,8 +7149,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + spitfire_aa.Geometry = smallTurret motionalarmsensor.Name = "motionalarmsensor" motionalarmsensor.Descriptor = "MotionSensors" @@ -7083,6 +7160,7 @@ object GlobalDefinitions { motionalarmsensor.Repairable = true motionalarmsensor.RepairIfDestroyed = false motionalarmsensor.DeployTime = Duration.create(1000, "ms") + motionalarmsensor.Geometry = sensor sensor_shield.Name = "sensor_shield" sensor_shield.Descriptor = "SensorShields" @@ -7091,6 +7169,7 @@ object GlobalDefinitions { sensor_shield.Repairable = true sensor_shield.RepairIfDestroyed = false sensor_shield.DeployTime = Duration.create(5000, "ms") + sensor_shield.Geometry = sensor tank_traps.Name = "tank_traps" tank_traps.Descriptor = "TankTraps" @@ -7107,8 +7186,9 @@ object GlobalDefinitions { Damage1 = 10 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + tank_traps.Geometry = GeometryForm.representByCylinder(radius = 2.89680997f, height = 3.57812f) val fieldTurretConverter = new FieldTurretConverter portable_manned_turret.Name = "portable_manned_turret" @@ -7117,10 +7197,11 @@ object GlobalDefinitions { portable_manned_turret.Damageable = true portable_manned_turret.Repairable = true portable_manned_turret.RepairIfDestroyed = false - portable_manned_turret.MountPoints += 1 -> 0 - portable_manned_turret.MountPoints += 2 -> 0 - portable_manned_turret.Weapons += 1 -> new mutable.HashMap() - portable_manned_turret.Weapons(1) += TurretUpgrade.None -> energy_gun + portable_manned_turret.controlledWeapons += 0 -> 1 + portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun + portable_manned_turret.MountPoints += 1 -> MountInfo(0) + portable_manned_turret.MountPoints += 2 -> MountInfo(0) portable_manned_turret.ReserveAmmunition = true portable_manned_turret.FactionLocked = true portable_manned_turret.Packet = fieldTurretConverter @@ -7134,8 +7215,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + portable_manned_turret.Geometry = largeTurret portable_manned_turret_nc.Name = "portable_manned_turret_nc" portable_manned_turret_nc.Descriptor = "FieldTurrets" @@ -7143,10 +7225,11 @@ object GlobalDefinitions { portable_manned_turret_nc.Damageable = true portable_manned_turret_nc.Repairable = true portable_manned_turret_nc.RepairIfDestroyed = false - portable_manned_turret_nc.MountPoints += 1 -> 0 - portable_manned_turret_nc.MountPoints += 2 -> 0 - portable_manned_turret_nc.Weapons += 1 -> new mutable.HashMap() - portable_manned_turret_nc.Weapons(1) += TurretUpgrade.None -> energy_gun_nc + portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc + portable_manned_turret_nc.controlledWeapons += 0 -> 1 + portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0) portable_manned_turret_nc.ReserveAmmunition = true portable_manned_turret_nc.FactionLocked = true portable_manned_turret_nc.Packet = fieldTurretConverter @@ -7160,8 +7243,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + portable_manned_turret_nc.Geometry = largeTurret portable_manned_turret_tr.Name = "portable_manned_turret_tr" portable_manned_turret_tr.Descriptor = "FieldTurrets" @@ -7169,10 +7253,11 @@ object GlobalDefinitions { portable_manned_turret_tr.Damageable = true portable_manned_turret_tr.Repairable = true portable_manned_turret_tr.RepairIfDestroyed = false - portable_manned_turret_tr.MountPoints += 1 -> 0 - portable_manned_turret_tr.MountPoints += 2 -> 0 - portable_manned_turret_tr.Weapons += 1 -> new mutable.HashMap() - portable_manned_turret_tr.Weapons(1) += TurretUpgrade.None -> energy_gun_tr + portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr + portable_manned_turret_tr.controlledWeapons += 0 -> 1 + portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0) portable_manned_turret_tr.ReserveAmmunition = true portable_manned_turret_tr.FactionLocked = true portable_manned_turret_tr.Packet = fieldTurretConverter @@ -7186,8 +7271,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + portable_manned_turret_tr.Geometry = largeTurret portable_manned_turret_vs.Name = "portable_manned_turret_vs" portable_manned_turret_vs.Descriptor = "FieldTurrets" @@ -7195,10 +7281,11 @@ object GlobalDefinitions { portable_manned_turret_vs.Damageable = true portable_manned_turret_vs.Repairable = true portable_manned_turret_vs.RepairIfDestroyed = false - portable_manned_turret_vs.MountPoints += 1 -> 0 - portable_manned_turret_vs.MountPoints += 2 -> 0 - portable_manned_turret_vs.Weapons += 1 -> new mutable.HashMap() - portable_manned_turret_vs.Weapons(1) += TurretUpgrade.None -> energy_gun_vs + portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs + portable_manned_turret_vs.controlledWeapons += 0 -> 1 + portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0) portable_manned_turret_vs.ReserveAmmunition = true portable_manned_turret_vs.FactionLocked = true portable_manned_turret_vs.Packet = fieldTurretConverter @@ -7212,8 +7299,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + portable_manned_turret_vs.Geometry = largeTurret deployable_shield_generator.Name = "deployable_shield_generator" deployable_shield_generator.Descriptor = "ShieldGenerators" @@ -7223,6 +7311,7 @@ object GlobalDefinitions { deployable_shield_generator.RepairIfDestroyed = false deployable_shield_generator.DeployTime = Duration.create(6000, "ms") deployable_shield_generator.Model = ComplexDeployableResolutions.calculate + deployable_shield_generator.Geometry = GeometryForm.representByCylinder(radius = 0.6562f, height = 2.17188f) router_telepad_deployable.Name = "router_telepad_deployable" router_telepad_deployable.MaxHealth = 100 @@ -7232,6 +7321,7 @@ object GlobalDefinitions { router_telepad_deployable.DeployCategory = DeployableCategory.Telepads router_telepad_deployable.Packet = new TelepadDeployableConverter router_telepad_deployable.Model = SimpleResolutions.calculate + router_telepad_deployable.Geometry = GeometryForm.representByRaisedSphere(radius = 1.2344f) internal_router_telepad_deployable.Name = "router_telepad_deployable" internal_router_telepad_deployable.MaxHealth = 1 @@ -7240,12 +7330,30 @@ object GlobalDefinitions { internal_router_telepad_deployable.DeployTime = Duration.create(1, "ms") internal_router_telepad_deployable.DeployCategory = DeployableCategory.Telepads internal_router_telepad_deployable.Packet = new InternalTelepadDeployableConverter + + special_emp.Name = "emp" + special_emp.MaxHealth = 1 + special_emp.Damageable = false + special_emp.Repairable = false + special_emp.DeployCategory = DeployableCategory.Mines + special_emp.explodes = true + special_emp.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 5f + AdditionalEffect = true + Modifiers = MaxDistanceCutoff + } } /** * Initialize `Miscellaneous` globals. */ private def initMiscellaneous(): Unit = { + val vterm = GeometryForm.representByCylinder(radius = 1.03515f, height = 1.09374f) _ + ams_respawn_tube.Name = "ams_respawn_tube" ams_respawn_tube.Delay = 10 ams_respawn_tube.SpecificPointFunc = SpawnPoint.AMS @@ -7286,9 +7394,10 @@ object GlobalDefinitions { order_terminal.MaxHealth = 500 order_terminal.Damageable = true order_terminal.Repairable = true - order_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + order_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) order_terminal.RepairIfDestroyed = true order_terminal.Subtract.Damage1 = 8 + order_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8438f, height = 1.3f) order_terminala.Name = "order_terminala" order_terminala.Tab += 0 -> OrderTerminalDefinition.EquipmentPage( @@ -7350,16 +7459,18 @@ object GlobalDefinitions { cert_terminal.MaxHealth = 500 cert_terminal.Damageable = true cert_terminal.Repairable = true - cert_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + cert_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) cert_terminal.RepairIfDestroyed = true cert_terminal.Subtract.Damage1 = 8 + cert_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.66405f, height = 1.09374f) implant_terminal_mech.Name = "implant_terminal_mech" implant_terminal_mech.MaxHealth = 1500 //TODO 1000; right now, 1000 (mech) + 500 (interface) implant_terminal_mech.Damageable = true implant_terminal_mech.Repairable = true - implant_terminal_mech.autoRepair = AutoRepairStats(1.6f, 5000, 2400, 0.5f) //ori. 1, 5000, 2400, 0.5f + implant_terminal_mech.autoRepair = AutoRepairStats(1.6f, 5000, 2400, 0.5f) implant_terminal_mech.RepairIfDestroyed = true + implant_terminal_mech.Geometry = GeometryForm.representByCylinder(radius = 2.7813f, height = 6.4375f) implant_terminal_interface.Name = "implant_terminal_interface" implant_terminal_interface.Tab += 0 -> OrderTerminalDefinition.ImplantPage(ImplantTerminalDefinition.implants) @@ -7368,6 +7479,7 @@ object GlobalDefinitions { implant_terminal_interface.Repairable = true implant_terminal_interface.autoRepair = AutoRepairStats(1, 5000, 200, 1) //TODO amount and drain are default? undefined? implant_terminal_interface.RepairIfDestroyed = true + //TODO will need geometry when Damageable = true ground_vehicle_terminal.Name = "ground_vehicle_terminal" ground_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7378,9 +7490,10 @@ object GlobalDefinitions { ground_vehicle_terminal.MaxHealth = 500 ground_vehicle_terminal.Damageable = true ground_vehicle_terminal.Repairable = true - ground_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + ground_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) ground_vehicle_terminal.RepairIfDestroyed = true ground_vehicle_terminal.Subtract.Damage1 = 8 + ground_vehicle_terminal.Geometry = vterm air_vehicle_terminal.Name = "air_vehicle_terminal" air_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7391,9 +7504,10 @@ object GlobalDefinitions { air_vehicle_terminal.MaxHealth = 500 air_vehicle_terminal.Damageable = true air_vehicle_terminal.Repairable = true - air_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + air_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) air_vehicle_terminal.RepairIfDestroyed = true air_vehicle_terminal.Subtract.Damage1 = 8 + air_vehicle_terminal.Geometry = vterm dropship_vehicle_terminal.Name = "dropship_vehicle_terminal" dropship_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7404,9 +7518,10 @@ object GlobalDefinitions { dropship_vehicle_terminal.MaxHealth = 500 dropship_vehicle_terminal.Damageable = true dropship_vehicle_terminal.Repairable = true - dropship_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + dropship_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) dropship_vehicle_terminal.RepairIfDestroyed = true dropship_vehicle_terminal.Subtract.Damage1 = 8 + dropship_vehicle_terminal.Geometry = vterm vehicle_terminal_combined.Name = "vehicle_terminal_combined" vehicle_terminal_combined.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7417,9 +7532,10 @@ object GlobalDefinitions { vehicle_terminal_combined.MaxHealth = 500 vehicle_terminal_combined.Damageable = true vehicle_terminal_combined.Repairable = true - vehicle_terminal_combined.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + vehicle_terminal_combined.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) vehicle_terminal_combined.RepairIfDestroyed = true vehicle_terminal_combined.Subtract.Damage1 = 8 + vehicle_terminal_combined.Geometry = vterm vanu_air_vehicle_term.Name = "vanu_air_vehicle_term" vanu_air_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7430,7 +7546,7 @@ object GlobalDefinitions { vanu_air_vehicle_term.MaxHealth = 500 vanu_air_vehicle_term.Damageable = true vanu_air_vehicle_term.Repairable = true - vanu_air_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + vanu_air_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) vanu_air_vehicle_term.RepairIfDestroyed = true vanu_air_vehicle_term.Subtract.Damage1 = 8 @@ -7443,7 +7559,7 @@ object GlobalDefinitions { vanu_vehicle_term.MaxHealth = 500 vanu_vehicle_term.Damageable = true vanu_vehicle_term.Repairable = true - vanu_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + vanu_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) vanu_vehicle_term.RepairIfDestroyed = true vanu_vehicle_term.Subtract.Damage1 = 8 @@ -7456,9 +7572,10 @@ object GlobalDefinitions { bfr_terminal.MaxHealth = 500 bfr_terminal.Damageable = true bfr_terminal.Repairable = true - bfr_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + bfr_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) bfr_terminal.RepairIfDestroyed = true bfr_terminal.Subtract.Damage1 = 8 + bfr_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.92185f, height = 2.64693f) respawn_tube.Name = "respawn_tube" respawn_tube.Delay = 10 @@ -7467,9 +7584,10 @@ object GlobalDefinitions { respawn_tube.Damageable = true respawn_tube.DamageableByFriendlyFire = false respawn_tube.Repairable = true - respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 + respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) respawn_tube.RepairIfDestroyed = true respawn_tube.Subtract.Damage1 = 8 + respawn_tube.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f) respawn_tube_sanctuary.Name = "respawn_tube" respawn_tube_sanctuary.Delay = 10 @@ -7478,7 +7596,8 @@ object GlobalDefinitions { respawn_tube_sanctuary.Damageable = false //true? respawn_tube_sanctuary.DamageableByFriendlyFire = false respawn_tube_sanctuary.Repairable = true - respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 + respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) + //TODO will need geometry when Damageable = true respawn_tube_tower.Name = "respawn_tube_tower" respawn_tube_tower.Delay = 10 @@ -7487,9 +7606,10 @@ object GlobalDefinitions { respawn_tube_tower.Damageable = true respawn_tube_tower.DamageableByFriendlyFire = false respawn_tube_tower.Repairable = true - respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 + respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) respawn_tube_tower.RepairIfDestroyed = true respawn_tube_tower.Subtract.Damage1 = 8 + respawn_tube_tower.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f) teleportpad_terminal.Name = "teleportpad_terminal" teleportpad_terminal.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.routerTerminal) @@ -7505,8 +7625,9 @@ object GlobalDefinitions { medical_terminal.MaxHealth = 500 medical_terminal.Damageable = true medical_terminal.Repairable = true - medical_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + medical_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) medical_terminal.RepairIfDestroyed = true + medical_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.711f, height = 1.75f) adv_med_terminal.Name = "adv_med_terminal" adv_med_terminal.Interval = 500 @@ -7517,8 +7638,9 @@ object GlobalDefinitions { adv_med_terminal.MaxHealth = 750 adv_med_terminal.Damageable = true adv_med_terminal.Repairable = true - adv_med_terminal.autoRepair = AutoRepairStats(1.57894f, 5000, 2400, 0.5f) //orig. 1, 5000, 2400, 0.5f + adv_med_terminal.autoRepair = AutoRepairStats(1.57894f, 5000, 2400, 0.5f) adv_med_terminal.RepairIfDestroyed = true + adv_med_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8662125f, height = 3.47f) crystals_health_a.Name = "crystals_health_a" crystals_health_a.Interval = 500 @@ -7545,7 +7667,7 @@ object GlobalDefinitions { portable_med_terminal.MaxHealth = 500 portable_med_terminal.Damageable = false //TODO actually true portable_med_terminal.Repairable = false - portable_med_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + portable_med_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) pad_landing_frame.Name = "pad_landing_frame" pad_landing_frame.Interval = 1000 @@ -7595,6 +7717,10 @@ object GlobalDefinitions { door.Damageable = false door.Repairable = false + gr_door_mb_orb.Name = "gr_door_mb_orb" + gr_door_mb_orb.Damageable = false + gr_door_mb_orb.Repairable = false + resource_silo.Name = "resource_silo" resource_silo.Damageable = false resource_silo.Repairable = false @@ -7659,13 +7785,14 @@ object GlobalDefinitions { manned_turret.Damageable = true manned_turret.DamageDisablesAt = 0 manned_turret.Repairable = true - manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.5f) //orig. 1, 10000, 1600, 0.5f + manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.5f) manned_turret.RepairIfDestroyed = true - manned_turret.Weapons += 1 -> new mutable.HashMap() - manned_turret.Weapons(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan - manned_turret.Weapons(1) += TurretUpgrade.AVCombo -> phalanx_avcombo - manned_turret.Weapons(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo - manned_turret.MountPoints += 1 -> 0 + manned_turret.WeaponPaths += 1 -> new mutable.HashMap() + manned_turret.WeaponPaths(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan + manned_turret.WeaponPaths(1) += TurretUpgrade.AVCombo -> phalanx_avcombo + manned_turret.WeaponPaths(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo + manned_turret.controlledWeapons += 0 -> 1 + manned_turret.MountPoints += 1 -> MountInfo(0) manned_turret.FactionLocked = true manned_turret.ReserveAmmunition = false manned_turret.explodes = true @@ -7675,22 +7802,25 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 5 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + manned_turret.Geometry = GeometryForm.representByCylinder(radius = 1.2695f, height = 4.042f) vanu_sentry_turret.Name = "vanu_sentry_turret" vanu_sentry_turret.MaxHealth = 1500 vanu_sentry_turret.Damageable = true vanu_sentry_turret.DamageDisablesAt = 0 vanu_sentry_turret.Repairable = true - vanu_sentry_turret.autoRepair = AutoRepairStats(3.27272f, 10000, 1000, 0.5f) //orig. 3, 10000, 1000, 0.5f + vanu_sentry_turret.autoRepair = AutoRepairStats(3.27272f, 10000, 1000, 0.5f) vanu_sentry_turret.RepairIfDestroyed = true - vanu_sentry_turret.Weapons += 1 -> new mutable.HashMap() - vanu_sentry_turret.Weapons(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon - vanu_sentry_turret.MountPoints += 1 -> 0 - vanu_sentry_turret.MountPoints += 2 -> 0 + vanu_sentry_turret.WeaponPaths += 1 -> new mutable.HashMap() + vanu_sentry_turret.WeaponPaths(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon + vanu_sentry_turret.controlledWeapons += 0 -> 1 + vanu_sentry_turret.MountPoints += 1 -> MountInfo(0) + vanu_sentry_turret.MountPoints += 2 -> MountInfo(0) vanu_sentry_turret.FactionLocked = false vanu_sentry_turret.ReserveAmmunition = false + vanu_sentry_turret.Geometry = GeometryForm.representByCylinder(radius = 1.76311f, height = 3.984375f) painbox.Name = "painbox" painbox.alwaysOn = false @@ -7765,7 +7895,7 @@ object GlobalDefinitions { generator.Damageable = true generator.DamageableByFriendlyFire = false generator.Repairable = true - generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1) //orig. 1, 5000, 875, 1 + generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1) generator.RepairDistance = 13.5f generator.RepairIfDestroyed = true generator.Subtract.Damage1 = 9 @@ -7773,12 +7903,17 @@ object GlobalDefinitions { generator.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 99999 - //DamageRadius should be 14, but 14 is insufficient for hitting the whole chamber; hence, ... - DamageRadius = 15.75f DamageRadiusMin = 14 + DamageRadius = 14.5f DamageAtEdge = 0.00002f - Modifiers = RadialDegrade - //damage is 99999 at 14m, dropping rapidly to ~1 at 15.75m + Modifiers = ExplodingRadialDegrade + //damage is 99999 at 14m, dropping rapidly to ~1 at 14.5m } + generator.Geometry = GeometryForm.representByCylinder(radius = 1.2617f, height = 9.14063f) + + obbasemesh.Name = "obbasemesh" + obbasemesh.Descriptor = "orbital_shuttle_pad" + obbasemesh.Damageable = false + obbasemesh.Repairable = false } } diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 80d2b027..4f1f8b33 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -79,7 +79,7 @@ class Player(var avatar: Avatar) def Faction: PlanetSideEmpire.Value = avatar.faction - def Sex: CharacterGender.Value = avatar.sex + def Sex: CharacterSex = avatar.sex def Head: Int = avatar.head diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index 91dd3896..76f4eabb 100644 --- a/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -11,7 +11,7 @@ import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.DamageableWeaponTurret import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.mount.MountableBehavior +import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.repair.RepairableWeaponTurret import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret} import net.psforever.objects.vital.damage.DamageCalculations @@ -25,8 +25,6 @@ class TurretDeployable(tdef: TurretDeployableDefinition) with Hackable { WeaponTurret.LoadDefinition(this) - def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap - override def Definition = tdef } @@ -65,8 +63,7 @@ class TurretControl(turret: TurretDeployable) extends Actor with FactionAffinityBehavior.Check with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events - with MountableBehavior.TurretMount - with MountableBehavior.Dismount + with MountableBehavior with DamageableWeaponTurret with RepairableWeaponTurret { def MountableObject = turret @@ -91,6 +88,13 @@ class TurretControl(turret: TurretDeployable) case _ => ; } + override protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player): Boolean = { + (!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed + } + override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = { super.DestructionAwareness(target, cause) 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 ec3a0be3..633cb528 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -1,10 +1,10 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.definition.{SeatDefinition, ToolDefinition, VehicleDefinition} -import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} +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.Mountable +import net.psforever.objects.serverobject.mount.{Seat, SeatDefinition} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.aura.AuraContainer @@ -18,7 +18,6 @@ import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} -import scala.annotation.tailrec import scala.concurrent.duration.FiniteDuration import scala.util.{Success, Try} @@ -33,7 +32,7 @@ import scala.util.{Success, Try} * The `Map` of `Utility` objects is given using the same inventory index positions. * Positive indices and zero are considered "represented" and must be assigned a globally unique identifier * and must be present in the containing vehicle's `ObjectCreateMessage` packet. - * The index is the seat position, reflecting the position in the zero-index inventory. + * The index is the mount position, reflecting the position in the zero-index inventory. * Negative indices are expected to be excluded from this conversion. * The value of the negative index does not have a specific meaning.
*
@@ -44,27 +43,27 @@ import scala.util.{Success, Try} * The driver is the only player that can access a vehicle's saved loadouts through a repair/rearm silo * and can procure equipment from the said silo. * The owner of a vehicle and the driver of a vehicle as mostly interchangeable terms for this reason - * and it can be summarized that the player who has access to the driver seat meets the qualifications for the "owner" - * so long as that player is the last person to have sat in that seat. - * All previous ownership information is replaced just as soon as someone else sits in the driver's seat. + * and it can be summarized that the player who has access to the driver mount meets the qualifications for the "owner" + * so long as that player is the last person to have sat in that mount. + * All previous ownership information is replaced just as soon as someone else sits in the driver's mount. * Ownership is also transferred as players die and respawn (from and to the same client) * and when they leave a continent without taking the vehicle they currently own with them. * (They also lose ownership when they leave the game, of course.)
*
* All seats have vehicle-level properties on top of their own internal properties. - * A seat has a glyph projected onto the ground when the vehicle is not moving - * that is used to mark where the seat can be accessed, as well as broadcasting the current access condition of the seat. + * A mount has a glyph projected onto the ground when the vehicle is not moving + * that is used to mark where the mount can be accessed, as well as broadcasting the current access condition of the mount. * As indicated previously, seats are composed into categories and the categories used to control access. - * The "driver" group has already been mentioned and is usually composed of a single seat, the "first" one. - * The driver seat is typically locked to the person who can sit in it - the owner - unless manually unlocked. - * Any seat besides the "driver" that has a weapon controlled from the seat is called a "gunner" seats. - * Any other seat besides the "driver" seat and "gunner" seats is called a "passenger" seat. + * The "driver" group has already been mentioned and is usually composed of a single mount, the "first" one. + * The driver mount is typically locked to the person who can sit in it - the owner - unless manually unlocked. + * Any mount besides the "driver" that has a weapon controlled from the mount is called a "gunner" seats. + * Any other mount besides the "driver" mount and "gunner" seats is called a "passenger" mount. * All of these seats are typically unlocked normally. - * The "trunk" also counts as an access group even though it is not directly attached to a seat and starts as "locked." + * The "trunk" also counts as an access group even though it is not directly attached to a mount and starts as "locked." * The categories all have their own glyphs, * sharing a red cross glyph as a "can not access" state, * and may also use their lack of visibility to express state. - * In terms of individual access, each seat can have its current occupant ejected, save for the driver's seat. + * In terms of individual access, each mount can have its current occupant ejected, save for the driver's mount. * @see `Vehicle.EquipmentUtilities` * @param vehicleDef the vehicle's definition entry; * stores and unloads pertinent information about the `Vehicle`'s configuration; @@ -72,11 +71,10 @@ import scala.util.{Success, Try} */ class Vehicle(private val vehicleDef: VehicleDefinition) extends AmenityOwner + with MountableWeapons with InteractsWithZoneEnvironment with Hackable with FactionAffinity - with Mountable - with MountedWeapons with Deployment with Vitality with OwnableByPlayer @@ -90,19 +88,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition) private var decal: Int = 0 private var trunkAccess: Option[PlanetSideGUID] = None private var jammered: Boolean = false + private var cloaked: Boolean = false - private var flying: Boolean = false + private var flying: Option[Int] = None private var capacitor: Int = 0 /** * Permissions control who gets to access different parts of the vehicle; - * the groups are Driver (seat), Gunner (seats), Passenger (seats), and the Trunk + * the groups are Driver (mount), Gunner (seats), Passenger (seats), and the Trunk */ private val groupPermissions: Array[VehicleLockState.Value] = Array(VehicleLockState.Locked, VehicleLockState.Empire, VehicleLockState.Empire, VehicleLockState.Locked) - private var seats: Map[Int, Seat] = Map.empty private var cargoHolds: Map[Int, Cargo] = Map.empty - private var weapons: Map[Int, EquipmentSlot] = Map.empty private var utilities: Map[Int, Utility] = Map() private val trunk: GridInventory = GridInventory() @@ -198,9 +195,13 @@ class Vehicle(private val vehicleDef: VehicleDefinition) Cloaked } - def Flying: Boolean = flying + def isFlying: Boolean = flying.nonEmpty - def Flying_=(isFlying: Boolean): Boolean = { + def Flying: Option[Int] = flying + + def Flying_=(isFlying: Int): Option[Int] = Flying_=(Some(isFlying)) + + def Flying_=(isFlying: Option[Int]): Option[Int] = { flying = isFlying Flying } @@ -226,17 +227,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition) Capacitor } - /** - * Given the index of an entry mounting point, return the infantry-accessible `Seat` associated with it. - * @param mountPoint an index representing the seat position / mounting point - * @return a seat number, or `None` - */ - def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = { - Definition.MountPoints.get(mountPoint) - } - - def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap - /** * What are the access permissions for a position on this vehicle, seats or trunk? * @param group the group index @@ -291,24 +281,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition) None } - /** - * Get the seat at the index. - * The specified "seat" can only accommodate a player as opposed to weapon mounts which share the same indexing system. - * @param seatNumber an index representing the seat position / mounting point - * @return a `Seat`, or `None` - */ - def Seat(seatNumber: Int): Option[Seat] = { - if (seatNumber >= 0 && seatNumber < this.seats.size) { - this.seats.get(seatNumber) - } else { - None - } - } - - def Seats: Map[Int, Seat] = { - seats - } - def CargoHold(cargoNumber: Int): Option[Cargo] = { if (cargoNumber >= 0) { this.cargoHolds.get(cargoNumber) @@ -322,12 +294,12 @@ class Vehicle(private val vehicleDef: VehicleDefinition) } def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = { - if (seatNumber == 0) { + if (seatNumber == 0) { //valid in almost all cases Some(AccessPermissionGroup.Driver) } else { Seat(seatNumber) match { - case Some(seat) => - seat.ControlledWeapon match { + case Some(_) => + Definition.controlledWeapons.get(seatNumber) match { case Some(_) => Some(AccessPermissionGroup.Gunner) case None => @@ -336,50 +308,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition) case None => CargoHold(seatNumber) match { case Some(_) => - Some(AccessPermissionGroup.Passenger) + Some(AccessPermissionGroup.Passenger) //TODO confirm this case None => - None + if (seatNumber >= trunk.Offset && seatNumber < trunk.Offset + trunk.TotalCapacity) { + Some(AccessPermissionGroup.Trunk) + } else { + None + } } } } } - def Weapons: Map[Int, EquipmentSlot] = weapons - - /** - * Get the weapon at the index. - * @param wepNumber an index representing the seat position / mounting point - * @return a weapon, or `None` - */ - def ControlledWeapon(wepNumber: Int): Option[Equipment] = { - weapons.get(wepNumber) match { - case Some(mount) => - mount.Equipment - case None => - None - } - } - - /** - * Given a player who may be an occupant, retrieve an number of the seat where this player is sat. - * @param player the player - * @return a seat number, or `None` if the `player` is not actually seated in this vehicle - */ - def PassengerInSeat(player: Player): Option[Int] = recursivePassengerInSeat(seats.iterator, player) - - @tailrec private def recursivePassengerInSeat(iter: Iterator[(Int, Seat)], player: Player): Option[Int] = { - if (!iter.hasNext) { - None - } else { - val (seatNumber, seat) = iter.next() - if (seat.Occupant.contains(player)) { - Some(seatNumber) - } else { - recursivePassengerInSeat(iter, player) - } - } - } - def Utilities: Map[Int, Utility] = utilities /** @@ -415,7 +355,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) def Inventory: GridInventory = trunk - def VisibleSlots: Set[Int] = weapons.keySet + def VisibleSlots: Set[Int] = weapons.keys.toSet override def Slot(slotNum: Int): EquipmentSlot = { weapons @@ -535,7 +475,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) def PrepareGatingManifest(): VehicleManifest = { val manifest = VehicleManifest(this) - seats.collect { case (index: Int, seat: Seat) if index > 0 => seat.Occupant = None } + seats.collect { case (index: Int, seat: Seat) if index > 0 => seat.unmount(seat.occupant) } vehicleGatingManifest = Some(manifest) previousVehicleGatingManifest = None manifest @@ -676,12 +616,12 @@ object Vehicle { //create seats vehicle.seats = vdef.Seats.map[Int, Seat] { case (num: Int, definition: SeatDefinition) => - num -> Seat(definition) + num -> new Seat(definition) }.toMap // create cargo holds vehicle.cargoHolds = vdef.Cargo.map[Int, Cargo] { case (num, definition) => - num -> Cargo(definition) + num -> new Cargo(definition) }.toMap //create utilities vehicle.utilities = vdef.Utilities.map[Int, Utility] { diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index ed5a328d..aed7c017 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -87,7 +87,7 @@ object Vehicles { /** * Disassociate a player from a vehicle that he owns. * The vehicle must exist in the game world on the specified continent. - * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat. + * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver mount. * This is the player side of vehicle ownership removal. * @param player the player */ @@ -96,7 +96,7 @@ object Vehicles { /** * Disassociate a player from a vehicle that he owns. * The vehicle must exist in the game world on the specified continent. - * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat. + * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver mount. * This is the player side of vehicle ownership removal. * @param player the player */ @@ -117,7 +117,7 @@ object Vehicles { /** * Disassociate a player from a vehicle that he owns without associating a different player as the owner. - * Set the vehicle's driver seat permissions and passenger and gunner seat permissions to "allow empire," + * Set the vehicle's driver mount permissions and passenger and gunner mount permissions to "allow empire," * then reload them for all clients. * This is the vehicle side of vehicle ownership removal. * @param player the player @@ -196,7 +196,7 @@ object Vehicles { val manifestPassengerResults = manifestPassengers.map { name => vzone.Players.exists(_.name.equals(name)) } manifestPassengerResults.forall(_ == true) && vehicle.CargoHolds.values - .collect { case hold if hold.isOccupied => AllGatedOccupantsInSameZone(hold.Occupant.get) } + .collect { case hold if hold.isOccupied => AllGatedOccupantsInSameZone(hold.occupant.get) } .forall(_ == true) case _ => false @@ -226,22 +226,22 @@ object Vehicles { * @param unk na; used by `HackMessage` as `unk5` */ def FinishHackingVehicle(target: Vehicle, hacker: Player, unk: Long)(): Unit = { - log.info(s"Vehicle guid: ${target.GUID} has been jacked") + log.info(s"${hacker.Name} has jacked a ${target.Definition.Name}") val zone = target.Zone // Forcefully dismount any cargo target.CargoHolds.values.foreach(cargoHold => { - cargoHold.Occupant match { + cargoHold.occupant match { case Some(cargo: Vehicle) => - cargo.Seats(0).Occupant match { + cargo.Seats(0).occupant match { case Some(_: Player) => CargoBehavior.HandleVehicleCargoDismount( target.Zone, cargo.GUID, - bailed = target.Flying, + bailed = target.isFlying, requestedByPassenger = false, kicked = true ) - case None => + case _ => log.error("FinishHackingVehicle: vehicle in cargo hold missing driver") CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, target.GUID, target, bailed = false, requestedByPassenger = false, kicked = true) } @@ -250,9 +250,9 @@ object Vehicles { }) // Forcefully dismount all seated occupants from the vehicle target.Seats.values.foreach(seat => { - seat.Occupant match { - case Some(tplayer) => - seat.Occupant = None + seat.occupant match { + case Some(tplayer: Player) => + seat.unmount(tplayer) tplayer.VehicleSeated = None if (tplayer.HasGUID) { zone.VehicleEvents ! VehicleServiceMessage( @@ -260,11 +260,11 @@ object Vehicles { VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID) ) } - case None => ; + case _ => ; } }) // If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed. - if (target.Definition.CanFly && target.Flying) { + if (target.Definition.CanFly && target.isFlying) { // todo: Should this force the vehicle to land in the same way as when a pilot bails with passengers on board? target.Actor ! Vehicle.Deconstruct() } else { // Otherwise handle ownership transfer as normal @@ -407,4 +407,21 @@ object Vehicles { case _ => ; } } + + /** + * Find the position and angle at which an ejected player will be placed once outside of the shuttle. + * Mainly for use with the proper high altitude rapid transport (HART) shuttle and it's corresponding HART building. + * @param obj the (shuttle) vehicle + * @param mountPoint the mount point that indicates a seat + * @return the position and angle + */ + def dismountShuttle(obj: Vehicle, mountPoint: Int): (Vector3, Float) = { + val shuttleAngle = obj.Orientation.z + val offset = { + val baseOffset = obj.MountPoints(mountPoint).positionOffset + Vector3.Rz(baseOffset.xy, shuttleAngle) + Vector3.z(baseOffset.z) + } + val turnAway = if (offset.x >= 0) -90f else 90f + (obj.Position + offset, (shuttleAngle + turnAway) % 360f) + } } diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index c823e4fe..9433f93b 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -72,7 +72,7 @@ case class Avatar( id: Int, name: String, faction: PlanetSideEmpire.Value, - sex: CharacterGender.Value, + sex: CharacterSex, head: Int, voice: CharacterVoice.Value, bep: Long = 0, diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 9c0fc7f1..5dd2c9ea 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -27,9 +27,11 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.objects.locker.LockerContainerControl import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} +import net.psforever.services.hart.ShuttleState import scala.concurrent.duration._ @@ -60,6 +62,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) + SetInteraction(EnvironmentAttribute.GantryDenialField, doInteractingWithGantryField) SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) private[this] val log = org.log4s.getLogger(player.Name) @@ -325,7 +328,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm ) case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => - log.info(s"wants to change equipment loadout to their option #${msg.unk1 + 1}") + log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}") val fallbackSubtype = 0 val fallbackSuit = ExoSuitType.Standard val originalSuit = player.ExoSuit @@ -368,7 +371,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm (exosuit, subtype) } else { log.warn( - s"no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead" + s"${player.Name} no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead" ) (fallbackSuit, fallbackSubtype) } @@ -708,6 +711,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //uninitialize implants avatarActor ! AvatarActor.DeinitializeImplants() + cause.adversarial match { + case Some(a) => + damageLog.info(s"DisplayDestroy: ${a.defender} was killed by ${a.attacker}") + case _ => + damageLog.info(s"DisplayDestroy: ${player.Name} killed ${player.Sex.pronounObject}self.") + } + // This would normally happen async as part of AvatarAction.Killed, but if it doesn't happen before deleting calling AvatarAction.ObjectDelete on the player the LLU will end up invisible to others if carried // Therefore, queue it up to happen first. events ! AvatarServiceMessage(nameChannel, AvatarAction.DropSpecialItem()) @@ -718,13 +728,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm ) //align client interface fields with state zone.GUID(target.VehicleSeated) match { case Some(obj: Mountable) => - //boot cadaver from seat internally (vehicle perspective) + //boot cadaver from mount internally (vehicle perspective) obj.PassengerInSeat(target) match { case Some(index) => - obj.Seats(index).Occupant = None + obj.Seats(index).unmount(target) case _ => ; } - //boot cadaver from seat on client + //boot cadaver from mount on client events ! AvatarServiceMessage( nameChannel, AvatarAction.SendResponse( @@ -1051,6 +1061,38 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm suicide() } + def doInteractingWithGantryField( + obj: PlanetSideServerObject, + body: PieceOfEnvironment, + data: Option[OxygenStateTarget] + ): Unit = { + import scala.concurrent.ExecutionContext.Implicits.global + val field = body.asInstanceOf[GantryDenialField] + val zone = player.Zone + (zone.GUID(field.obbasemesh) match { + case Some(pad : OrbitalShuttlePad) => zone.GUID(pad.shuttle) + case _ => None + }) match { + case Some(shuttle: Vehicle) + if shuttle.Flying.contains(ShuttleState.State11.id) || shuttle.Faction != player.Faction => + val (pos, zang) = Vehicles.dismountShuttle(shuttle, field.mountPoint) + shuttle.Zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + PlayerStateShiftMessage(ShiftState(0, pos, zang, None))) + ) + case Some(_: Vehicle) => + interactionTimer = context.system.scheduler.scheduleOnce( + delay = 250 milliseconds, + self, + InteractWithEnvironment(player, body, None) + ) + case _ => ; + //something configured incorrectly; no need to keep checking + } + } + /** * When out of water, the player is no longer suffocating. * The player does have to endure a recovery period to get back to normal, though. diff --git a/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala b/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala index 5413f325..0a2d2120 100644 --- a/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala +++ b/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala @@ -18,6 +18,8 @@ final case class PlayerSource( position: Vector3, orientation: Vector3, velocity: Option[Vector3], + crouching: Boolean, + jumping: Boolean, modifiers: ResistanceProfile ) extends SourceEntry { override def Name = name @@ -48,6 +50,8 @@ object PlayerSource { tplayer.Position, tplayer.Orientation, tplayer.Velocity, + tplayer.Crouching, + tplayer.Jumping, ExoSuitDefinition.Select(tplayer.ExoSuit, tplayer.Faction) ) } diff --git a/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala b/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala index da18c098..f99de884 100644 --- a/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala @@ -3,15 +3,17 @@ package net.psforever.objects.definition import net.psforever.objects.avatar.Avatars import net.psforever.objects.definition.converter.AvatarConverter +import net.psforever.objects.geometry.GeometryForm import net.psforever.objects.vital.VitalityDefinition /** - * The definition for game objects that look like other people, and also for players. - * @param objectId the object's identifier number + * The definition for game objects that look like players. + * @param objectId the object type number */ class AvatarDefinition(objectId: Int) extends ObjectDefinition(objectId) with VitalityDefinition { Avatars(objectId) //let throw NoSuchElementException Packet = AvatarDefinition.converter + Geometry = GeometryForm.representPlayerByCylinder(radius = 1.6f) } object AvatarDefinition { diff --git a/src/main/scala/net/psforever/objects/definition/CargoDefinition.scala b/src/main/scala/net/psforever/objects/definition/CargoDefinition.scala index 4aee68de..470c1aa1 100644 --- a/src/main/scala/net/psforever/objects/definition/CargoDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/CargoDefinition.scala @@ -1,35 +1,14 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2021 PSForever package net.psforever.objects.definition -import net.psforever.objects.vehicles.CargoVehicleRestriction +import net.psforever.objects.Vehicle +import net.psforever.objects.serverobject.mount.{LargeCargo, MountRestriction, MountableSpaceDefinition} -/** - * The definition for a cargo hold. - */ -class CargoDefinition extends BasicDefinition { - - /** a restriction on the type of exo-suit a person can wear */ - private var vehicleRestriction: CargoVehicleRestriction.Value = CargoVehicleRestriction.Small - - /** the user can escape while the vehicle is moving */ - private var bailable: Boolean = true +class CargoDefinition extends MountableSpaceDefinition[Vehicle] { Name = "cargo" + def occupancy: Int = 1 - def CargoRestriction: CargoVehicleRestriction.Value = { - this.vehicleRestriction - } + var restriction: MountRestriction[Vehicle] = LargeCargo - def CargoRestriction_=(restriction: CargoVehicleRestriction.Value): CargoVehicleRestriction.Value = { - this.vehicleRestriction = restriction - restriction - } - - def Bailable: Boolean = { - this.bailable - } - - def Bailable_=(canBail: Boolean): Boolean = { - this.bailable = canBail - canBail - } + var bailable: Boolean = true } diff --git a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala index 29e7609c..0df09165 100644 --- a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala @@ -3,6 +3,7 @@ package net.psforever.objects.definition import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.definition.converter.{ObjectCreateConverter, PacketConverter} +import net.psforever.objects.geometry.{Geometry3D, GeometryForm} import net.psforever.types.OxygenState /** @@ -76,5 +77,27 @@ abstract class ObjectDefinition(private val objectId: Int) extends BasicDefiniti UnderwaterLifespan() } + private var serverSplashTargetsCentroid: Boolean = false + + def ServerSplashTargetsCentroid: Boolean = serverSplashTargetsCentroid + + def ServerSplashTargetsCentroid_=(splash: Boolean): Boolean = { + serverSplashTargetsCentroid = splash + ServerSplashTargetsCentroid + } + + private var serverGeometry: Any => Geometry3D = GeometryForm.representByPoint() + + def Geometry: Any => Geometry3D = if (ServerSplashTargetsCentroid) { + GeometryForm.representByPoint() + } else { + serverGeometry + } + + def Geometry_=(func: Any => Geometry3D): Any => Geometry3D = { + serverGeometry = func + Geometry + } + def ObjectId: Int = objectId } diff --git a/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala b/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala deleted file mode 100644 index fc2c04a3..00000000 --- a/src/main/scala/net/psforever/objects/definition/SeatDefinition.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.definition - -import net.psforever.objects.vehicles.SeatArmorRestriction - -/** - * The definition for a seat. - */ -class SeatDefinition extends BasicDefinition { - - /** a restriction on the type of exo-suit a person can wear */ - private var armorRestriction: SeatArmorRestriction.Value = SeatArmorRestriction.NoMax - - /** the user can escape while the vehicle is moving */ - private var bailable: Boolean = false - - /** any controlled weapon */ - private var weaponMount: Option[Int] = None - Name = "seat" - - def ArmorRestriction: SeatArmorRestriction.Value = { - this.armorRestriction - } - - def ArmorRestriction_=(restriction: SeatArmorRestriction.Value): SeatArmorRestriction.Value = { - this.armorRestriction = restriction - restriction - } - - def Bailable: Boolean = { - this.bailable - } - - def Bailable_=(canBail: Boolean): Boolean = { - this.bailable = canBail - canBail - } - - def ControlledWeapon: Option[Int] = { - this.weaponMount - } - - def ControlledWeapon_=(wep: Int): Option[Int] = { - ControlledWeapon_=(Some(wep)) - } - - def ControlledWeapon_=(wep: Option[Int]): Option[Int] = { - this.weaponMount = wep - ControlledWeapon - } -} diff --git a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index d208c2c0..83a41a35 100644 --- a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -4,7 +4,7 @@ package net.psforever.objects.definition import net.psforever.objects.NtuContainerDefinition import net.psforever.objects.definition.converter.VehicleConverter import net.psforever.objects.inventory.InventoryTile -import net.psforever.objects.vehicles.{DestroyedVehicle, UtilityType} +import net.psforever.objects.vehicles.{DestroyedVehicle, MountableWeaponsDefinition, UtilityType} import net.psforever.objects.vital._ import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.resistance.ResistanceProfileMutators @@ -20,19 +20,14 @@ import scala.concurrent.duration._ */ class VehicleDefinition(objectId: Int) extends ObjectDefinition(objectId) + with MountableWeaponsDefinition with VitalityDefinition with NtuContainerDefinition with ResistanceProfileMutators with DamageResistanceModel { /** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */ private var maxShields: Int = 0 - /* key - seat index, value - seat object */ - private val seats: mutable.HashMap[Int, SeatDefinition] = mutable.HashMap[Int, SeatDefinition]() private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]() - /* key - entry point index, value - seat index */ - private val mountPoints: mutable.HashMap[Int, Int] = mutable.HashMap() - /* key - seat index (where this weapon attaches during object construction), value - the weapon on an EquipmentSlot */ - private val weapons: mutable.HashMap[Int, ToolDefinition] = mutable.HashMap[Int, ToolDefinition]() private var deployment: Boolean = false private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap() private val utilityOffsets: mutable.HashMap[Int, Vector3] = mutable.HashMap() @@ -44,8 +39,16 @@ class VehicleDefinition(objectId: Int) private var trunkLocation: Vector3 = Vector3.Zero private var canCloak: Boolean = false private var canFly: Boolean = false - private var canBeOwned: Boolean = true + /** whether the vehicle gains and/or maintains ownership based on access to the driver seat
+ * `Some(true)` - assign ownership upon the driver mount, maintains ownership after the driver dismounts
+ * `Some(false)` - assign ownership upon the driver mount, becomes unowned after the driver dismounts
+ * `None` - does not assign ownership
+ * Be cautious about using `None` as the client tends to equate the driver seat as the owner's seat for many vehicles + * and breaking from the client's convention either requires additional fields or just doesn't work. + */ + private var canBeOwned: Option[Boolean] = Some(true) private var serverVehicleOverrideSpeeds: (Int, Int) = (0, 0) + var undergoesDecay: Boolean = true private var deconTime: Option[FiniteDuration] = None private var maxCapacitor: Int = 0 private var destroyedModel: Option[DestroyedVehicle.Value] = None @@ -64,15 +67,13 @@ class VehicleDefinition(objectId: Int) MaxShields } - def Seats: mutable.HashMap[Int, SeatDefinition] = seats - def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo - def MountPoints: mutable.HashMap[Int, Int] = mountPoints + def CanBeOwned: Option[Boolean] = canBeOwned - def CanBeOwned: Boolean = canBeOwned + def CanBeOwned_=(ownable: Boolean): Option[Boolean] = CanBeOwned_=(Some(ownable)) - def CanBeOwned_=(ownable: Boolean): Boolean = { + def CanBeOwned_=(ownable: Option[Boolean]): Option[Boolean] = { canBeOwned = ownable CanBeOwned } @@ -91,8 +92,6 @@ class VehicleDefinition(objectId: Int) CanFly } - def Weapons: mutable.HashMap[Int, ToolDefinition] = weapons - def Deployment: Boolean = deployment def Deployment_=(deployable: Boolean): Boolean = { diff --git a/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala index 686e89dd..dcdf82fd 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala @@ -30,7 +30,7 @@ class CorpseConverter extends AvatarConverter { */ private def MakeAppearanceData(obj: Player): Int => CharacterAppearanceData = { val aa: Int => CharacterAppearanceA = CharacterAppearanceA( - BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, CharacterVoice.Mute), + BasicCharacterData(obj.Name, obj.Faction, CharacterSex.Male, 0, CharacterVoice.Mute), CommonFieldData( obj.Faction, bops = false, diff --git a/src/main/scala/net/psforever/objects/definition/converter/OrbitalShuttleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/OrbitalShuttleConverter.scala new file mode 100644 index 00000000..3535de84 --- /dev/null +++ b/src/main/scala/net/psforever/objects/definition/converter/OrbitalShuttleConverter.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.Vehicle +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class OrbitalShuttleConverter extends ObjectCreateConverter[Vehicle]() { + override def ConstructorData(obj: Vehicle): Try[OrbitalShuttleData] = { + Success(OrbitalShuttleData(obj.Faction, Some(PlacementData(obj.Position, obj.Orientation)))) + } + + override def DetailedConstructorData(obj: Vehicle): Try[OrbitalShuttleData] = + Failure(new Exception("OrbitalShuttleConverter should not be used to generate detailed OrbitalShuttleData (nothing should)")) +} diff --git a/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala index ced03f48..a7a23f7b 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala @@ -2,7 +2,7 @@ package net.psforever.objects.definition.converter import net.psforever.objects.Player -import net.psforever.objects.vehicles.Seat +import net.psforever.objects.serverobject.mount.Seat import net.psforever.packet.game.objectcreate.{InventoryItemData, ObjectClass, PlayerData, VehicleData} object SeatConverter { @@ -16,14 +16,14 @@ object SeatConverter { ) } - //TODO do not use for now; causes seat access permission issues with many passengers; may not mesh with workflows; GUID requirements + //TODO do not use for now; causes mount access permission issues with many passengers; may not mesh with workflows; GUID requirements def MakeSeats(seats: Map[Int, Seat], initialOffset: Long): List[InventoryItemData.InventoryItem] = { var offset = initialOffset seats .filter({ case (_, seat) => seat.isOccupied }) .map({ - case (index, seat) => - val player = seat.Occupant.get + case (index: Int, seat: Seat) => + val player = seat.occupant.get val entry = InventoryItemData(ObjectClass.avatar, player.GUID, index, SeatConverter.MakeSeat(player, offset)) offset += entry.bitsize entry diff --git a/src/main/scala/net/psforever/objects/definition/converter/VariantVehicleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/VariantVehicleConverter.scala index f30cafca..ed972465 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/VariantVehicleConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/VariantVehicleConverter.scala @@ -14,7 +14,7 @@ class VariantVehicleConverter extends VehicleConverter { */ Some( VariantVehicleData( - if (obj.Definition.CanFly && obj.Flying) 7 else 0 + if (obj.Definition.CanFly && obj.isFlying) 7 else 0 ) ) } diff --git a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 303fd4bf..596bf5c8 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -76,7 +76,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = { val offset: Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) - obj.Seats(0).Occupant match { + obj.Seats(0).occupant match { case Some(player) => List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset))) case None => diff --git a/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala b/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala index 0bf64cf7..ce74af5c 100644 --- a/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala +++ b/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala @@ -13,7 +13,7 @@ object EquipmentSize extends Enumeration { VehicleWeapon, //vehicle-mounted weapons BaseTurretWeapon, //common phalanx cannons, and cavern turrets BFRArmWeapon, //duel arm weapons for bfr - BFRGunnerWeapon, //gunner seat for bfr + BFRGunnerWeapon, //gunner mount for bfr Inventory //reserved = Value diff --git a/src/main/scala/net/psforever/objects/geometry/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala new file mode 100644 index 00000000..ce796b75 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +object Geometry { + def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = { + val diff = value1 - value2 + if (diff >= 0) diff <= off else diff > -off + } + + def equalVectors(value1: Vector3, value2: Vector3, off: Float = 0.001f): Boolean = { + equalFloats(value1.x, value2.x, off) && + equalFloats(value1.y, value2.y, off) && + equalFloats(value1.z, value2.z, off) + } + + def closeToInsignificance(d: Float, epsilon: Float = 10f): Float = { + val ulp = math.ulp(epsilon) + math.signum(d) match { + case -1f => + val n = math.abs(d) + val p = math.abs(n - n.toInt) + if (p < ulp || d > ulp) d + p else d + case _ => + val p = math.abs(d - d.toInt) + if (p < ulp || d < ulp) d - p else d + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala new file mode 100644 index 00000000..8614163e --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala @@ -0,0 +1,126 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player} +import net.psforever.types.{ExoSuitType, Vector3} + +object GeometryForm { + /** this point can not be used for purposes of geometric representation */ + lazy val invalidPoint: Point3D = Point3D(Float.MinValue, Float.MinValue, Float.MinValue) + /** this cylinder can not be used for purposes of geometric representation */ + lazy val invalidCylinder: Cylinder = Cylinder(invalidPoint.asVector3, Vector3.Zero, Float.MinValue, 0) + + /** + * The geometric representation is the entity's centroid. + * @param o the entity + * @return the representation + */ + def representByPoint()(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => Point3D(p.Position) + case s: SourceEntry => Point3D(s.Position) + case _ => invalidPoint + } + } + + /** + * The geometric representation is a sphere around the entity's centroid + * positioned following the axis of rotation (the entity's base). + * @param radius how wide a hemisphere is + * @param o the entity + * @return the representation + */ + def representBySphere(radius: Float)(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => + Sphere(p.Position, radius) + case s: SourceEntry => + Sphere(s.Position, radius) + case _ => + Sphere(invalidPoint, radius) + } + } + + /** + * The geometric representation is a sphere around the entity's centroid + * positioned following the axis of rotation (the entity's base). + * @param radius how wide a hemisphere is + * @param o the entity + * @return the representation + */ + def representByRaisedSphere(radius: Float)(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => + Sphere(p.Position + Vector3.relativeUp(p.Orientation) * radius, radius) + case s: SourceEntry => + Sphere(s.Position + Vector3.relativeUp(s.Orientation) * radius, radius) + case _ => + Sphere(invalidPoint, radius) + } + } + + /** + * The geometric representation is a cylinder around the entity's base. + * @param radius half the distance across + * @param height how tall the cylinder is (the distance of the top to the base) + * @param o the entity + * @return the representation + */ + def representByCylinder(radius: Float, height: Float)(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => Cylinder(p.Position, Vector3.relativeUp(p.Orientation), radius, height) + case s: SourceEntry => Cylinder(s.Position, Vector3.relativeUp(s.Orientation), radius, height) + case _ => invalidCylinder + } + } + + /** + * The geometric representation is a cylinder around the entity's base + * if the target represents a player entity. + * @param radius a measure of the player's bulk + * @param o the entity + * @return the representation + */ + def representPlayerByCylinder(radius: Float)(o: Any): Geometry3D = { + o match { + case p: Player => + val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.25f else 0f + Cylinder( + p.Position, + radius + radialOffset, + GlobalDefinitions.MaxDepth(p) + ) + case p: PlayerSource => + val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.125f else 0f + val heightOffset = if(p.crouching) 1.093750f else GlobalDefinitions.avatar.MaxDepth + Cylinder( + p.Position, + radius + radialOffset, + heightOffset + ) + case _ => + invalidCylinder + } + } + + /** + * The geometric representation is a cylinder around the entity's base + * as if the target is displaced from the ground at an expected (fixed?) distance. + * @param radius half the distance across + * @param height how tall the cylinder is (the distance of the top to the base) + * @param hoversAt how far off the base coordinates the actual cylinder begins + * @param o the entity + * @return the representation + */ + def representHoveringEntityByCylinder(radius: Float, height: Float, hoversAt: Float)(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => + Cylinder(p.Position, Vector3.relativeUp(p.Orientation), radius, height) + case s: SourceEntry => + Cylinder(s.Position, Vector3.relativeUp(s.Orientation), radius, height) + case _ => + invalidCylinder + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala new file mode 100644 index 00000000..738e4596 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala @@ -0,0 +1,433 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +/** + * Basic interface for all geometry. + */ +trait PrimitiveGeometry { + /** + * The centroid of the geometry. + * @return a point + */ + def center: Point + + /** + * Find a point on the exterior of the geometry if a line was drawn outwards from the centroid. + * What counts as "the exterior" is limited to the complexity of the geometry. + * @param v the vector in the direction of the point on the exterior + * @return a point + */ + def pointOnOutside(v: Vector3) : Point +} + +//trait Geometry2D extends PrimitiveGeometry { +// def center: Point2D +// +// def pointOnOutside(v: Vector3): Point2D = center +//} + +/** + * Basic interface of all three-dimensional geometry. + * For the only real requirement for a hree-dimensional geometric figure is that it has three components of position + * and an equal number of components demonstrating equal that said dimensionality. + */ +trait Geometry3D extends PrimitiveGeometry { + def center: Point3D + + def pointOnOutside(v: Vector3): Point3D = center +} + +/** + * Characteristics of a geometric figure with only three coordinates to define a position. + */ +trait Point { + /** + * Transform the point into the common interchangeable format for coordinates. + * They're very similar, anyway. + * @return a `Vector3` entity of the same denomination + */ + def asVector3: Vector3 +} + +/** + * Characteristics of a geometric figure defining a direction or a progressive change in coordinates. + */ +trait Slope { + /** + * The slope itself. + * @return a `Vector3` entity + */ + def d: Vector3 + + /** + * How long the slope goes on for. + * @return The length of the slope + */ + def length: Float +} + +object Slope { + /** + * On occasions, the defined slope should have a length of one unit. + * It is a unit vector. + * @param v the input slope as a `Vector3` entity + * @throws `AssertionError` if the length is more or less than 1. + */ + def assertUnitVector(v: Vector3): Unit = { + assert({ + val mag = Vector3.Magnitude(v) + mag - 0.05f < 1f && mag + 0.05f > 1f + }, "not a unit vector") + } +} + +/** + * Characteristics of a geometric figure indicating an infinite slope - a mathematical line. + * The slope is always a unit vector. + * The point that assists to define the line is a constraint that the line must pass through. + */ +trait Line extends Slope { + Slope.assertUnitVector(d) + + def p: Point + + /** + * The length of a mathematical line is infinite. + * @return The length of the slope + */ + def length: Float = Float.PositiveInfinity +} + +/** + * Characteristics of a geometric figure that have two endpoints, defining a fixed-length slope. + */ +trait Segment extends Slope { + /** The first point, considered the "start". */ + def p1: Point + /** The second point, considered the "end". */ + def p2: Point + + def length: Float = Vector3.Magnitude(d) + + /** + * Transform the segment into a matheatical line of the same slope. + * @return + */ + def asLine: PrimitiveGeometry +} + +/** + * The instance of a geometric coordinate position. + * @see `Vector3` + * @param x the 'x' coordinate of the position + * @param y the 'y' coordinate of the position + * @param z the 'z' coordinate of the position + */ +final case class Point3D(x: Float, y: Float, z: Float) extends Geometry3D with Point { + def center: Point3D = this + + def asVector3: Vector3 = Vector3(x, y, z) +} + +object Point3D { + /** + * An overloaded constructor that assigns world origin coordinates. + * @return a `Point3D` entity + */ + def apply(): Point3D = Point3D(0,0,0) + + /** + * An overloaded constructor that uses the same coordinates from a `Vector3` entity. + * @param v the entity with the corresponding points + * @return a `Point3D` entity + */ + def apply(v: Vector3): Point3D = Point3D(v.x, v.y, v.z) +} + +/** + * The instance of a geometric coordinate position and a specific direction from that position. + * Rays are like mathematical lines in that they have infinite length; + * but, that infinite length is only expressed in a single direction, + * rather than proceeding in both a direction and its opposite direction from a target point. + * Infinity just be like that. + * Additionally, the point is not merely any point on the ray used to assist defining it + * and is instead considered the clearly-defined origin of the ray. + * @param p the point of origin + * @param d the direction + */ +final case class Ray3D(p: Point3D, d: Vector3) extends Geometry3D with Line { + def center: Point3D = p +} + +object Ray3D { + /** + * An overloaded constructor that uses individual coordinates. + * @param x the 'x' coordinate of the position + * @param y the 'y' coordinate of the position + * @param z the 'z' coordinate of the position + * @param d the direction + * @return a `Ray3D` entity + */ + def apply(x: Float, y: Float, z: Float, d: Vector3): Ray3D = Ray3D(Point3D(x,y,z), d) + + /** + * An overloaded constructor that uses a `Vector3` entity to express coordinates. + * @param v the coordinates of the position + * @param d the direction + * @return a `Ray3D` entity + */ + def apply(v: Vector3, d: Vector3): Ray3D = Ray3D(Point3D(v.x, v.y, v.z), d) +} + +/** + * The instance of a geometric coordinate position and a specific direction from that position. + * Mathematical lines have infinite length and their slope is represented as a unit vector. + * The point is merely a point used to assist in defining the line. + * @param p the point of origin + * @param d the direction + */ +final case class Line3D(p: Point3D, d: Vector3) extends Geometry3D with Line { + def center: Point3D = p +} + +object Line3D { + /** + * An overloaded constructor that uses individual coordinates. + * @param x the 'x' coordinate of the position + * @param y the 'y' coordinate of the position + * @param z the 'z' coordinate of the position + * @param d the direction + * @return a `Line3D` entity + */ + def apply(x: Float, y: Float, z: Float, d: Vector3): Line3D = { + Line3D(Point3D(x,y,z), d) + } + + /** + * An overloaded constructor that uses a pair of individual coordinates + * and uses their difference to produce a unit vector to define a direction. + * @param ax the 'x' coordinate of the position + * @param ay the 'y' coordinate of the position + * @param az the 'z' coordinate of the position + * @param bx the 'x' coordinate of a destination position + * @param by the 'y' coordinate of a destination position + * @param bz the 'z' coordinate of a destination position + * @return a `Line3D` entity + */ + def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Line3D = { + Line3D(Point3D(ax, ay, az), Vector3.Unit(Vector3(bx-ax, by-ay, bz-az))) + } + + /** + * An overloaded constructor that uses a pair of points + * and uses their difference to produce a unit vector to define a direction. + * @param p1 the coordinates of the position + * @param p2 the coordinates of a destination position + * @return a `Line3D` entity + */ + def apply(p1: Point3D, p2: Point3D): Line3D = { + Line3D(p1, Vector3.Unit(Vector3(p2.x-p1.x, p2.y-p1.y, p2.z-p1.z))) + } +} + +/** + * The instance of a limited span between two geometric coordinate positions, called "endpoints". + * Unlike mathematical lines, slope is treated the same as the vector leading from one point to the other + * and is the length of the segment. + * @param p1 a point + * @param p2 another point + */ +final case class Segment3D(p1: Point3D, p2: Point3D) extends Geometry3D with Segment { + /** + * The center point of a segment is a position that is equally in between both endpoints. + * @return a point + */ + def center: Point3D = Point3D((p2.asVector3 + p1.asVector3) * 0.5f) + + def d: Vector3 = p2.asVector3 - p1.asVector3 + + def asLine: Line3D = Line3D(p1, Vector3.Unit(d)) +} + +object Segment3D { + /** + * An overloaded constructor that uses a pair of individual coordinates + * and uses their difference to define a direction. + * @param ax the 'x' coordinate of the position + * @param ay the 'y' coordinate of the position + * @param az the 'z' coordinate of the position + * @param bx the 'x' coordinate of a destination position + * @param by the 'y' coordinate of a destination position + * @param bz the 'z' coordinate of a destination position + * @return a `Segment3D` entity + */ + def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Segment3D = { + Segment3D(Point3D(ax, ay, az), Point3D(bx, by, bz)) + } + + /** + * An overloaded constructor. + * @param p the point of origin + * @param d the direction and distance (of the second point) + */ + def apply(p: Point3D, d: Vector3): Segment3D = { + Segment3D(p, Point3D(p.x + d.x, p.y + d.y, p.z + d.z)) + } + + /** + * An overloaded constructor that uses individual coordinates. + * @param x the 'x' coordinate of the position + * @param y the 'y' coordinate of the position + * @param z the 'z' coordinate of the position + * @param d the direction + * @return a `Segment3D` entity + */ + def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = { + Segment3D(Point3D(x, y, z), Point3D(x + d.x, y + d.y, z + d.z)) + } +} + +/** + * The instance of a volumetric region that encapsulates all points within a certain distance of a central point. + * (That's what a sphere is.) + * A sphere has no real "top", "base", or "side" as all directions are described the same. + * @param p the point + * @param radius a distance that spans all points in any direction from the central point + */ +final case class Sphere(p: Point3D, radius: Float) extends Geometry3D { + def center: Point3D = p + + /** + * Find a point on the exterior of the geometry if a line was drawn outwards from the centroid. + * All points that exist on the exterior of a sphere are on the surface of that sphere + * and are equally distant from the central point. + * @param v the vector in the direction of the point on the exterior + * @return a point + */ + override def pointOnOutside(v: Vector3): Point3D = { + val slope = Vector3.Unit(v) + val mult = radius / Vector3.Magnitude(slope) + Point3D(center.asVector3 + slope * mult) + } +} + +object Sphere { + /** + * An overloaded constructor that only defines the radius of the sphere + * and places it at the world origin. + * @param radius a distance around the world origin coordinates + * @return a `Sphere` entity + */ + def apply(radius: Float): Sphere = Sphere(Point3D(), radius) + + /** + * An overloaded constructor that uses individual coordinates to define the central point. + * * @param x the 'x' coordinate of the position + * * @param y the 'y' coordinate of the position + * * @param z the 'z' coordinate of the position + * @param radius a distance around the world origin coordinates + * @return a `Sphere` entity + */ + def apply(x: Float, y: Float, z: Float, radius: Float): Sphere = Sphere(Point3D(x,y,z), radius) + + /** + * An overloaded constructor that uses vector coordinates to define the central point. + * @param v the coordinates of the position + * @param radius a distance around the world origin coordinates + * @return a `Sphere` entity + */ + def apply(v: Vector3, radius: Float): Sphere = Sphere(Point3D(v), radius) +} + +/** + * The instance of a volumetric region that encapsulates all points within a certain distance of a central point. + * The region is characterized by a regular circular cross-section when observed from above or below + * and a flat top and a flat base when viewed from the side. + * The "base" is where the origin point is defined (at the center of a circular cross-section) + * and the "top" is discovered a `height` from the base along what the cylinder considers its `relativeUp` direction. + * @param p the point + * @param relativeUp what the cylinder considers its "up" direction + * @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction + * @param height the distance between the "base" and the "top" + */ +final case class Cylinder(p: Point3D, relativeUp: Vector3, radius: Float, height: Float) extends Geometry3D { + Slope.assertUnitVector(relativeUp) + + /** + * The center point of a cylinder is halfway between the "top" and the "base" along the direction of `relativeUp`. + * @return a point + */ + def center: Point3D = Point3D(p.asVector3 + relativeUp * height * 0.5f) + + /** + * Find a point on the exterior of the geometry if a line was drawn outwards from the centroid. + * A cylinder is composed of three clearly-defined regions on its exterior - + * two flat but circular surfaces that are the "top" and the "base" + * and a wrapped "sides" surface that defines all points connecting the "base" to the "top" + * along the `relativeUp` direction. + * The requested point may exist on any of these surfaces. + * @param v the vector in the direction of the point on the exterior + * @return a point + */ + override def pointOnOutside(v: Vector3): Point3D = { + val centerVector = center.asVector3 + val slope = Vector3.Unit(v) + val dotProdOfSlopeAndUp = Vector3.DotProduct(slope, relativeUp) + if (Geometry.equalFloats(dotProdOfSlopeAndUp, value2 = 1) || Geometry.equalFloats(dotProdOfSlopeAndUp, value2 = -1)) { + // very rare condition: 'slope' and 'relativeUp' are parallel or antiparallel + Point3D(centerVector + slope * height * 0.5f) + } else { + val acrossTopAndBase = slope - relativeUp * dotProdOfSlopeAndUp + val pointOnSide = centerVector + slope * (radius / Vector3.Magnitude(acrossTopAndBase)) + val pointOnBase = p.asVector3 + acrossTopAndBase * radius + val pointOnTop = pointOnBase + relativeUp * height + val fromPointOnTopToSide = Vector3.Unit(pointOnTop - pointOnSide) + val fromPointOnSideToBase = Vector3.Unit(pointOnSide - pointOnBase) + val target = if(Geometry.equalVectors(fromPointOnTopToSide, Vector3.Zero) || + Geometry.equalVectors(fromPointOnSideToBase, Vector3.Zero) || + Geometry.equalVectors(fromPointOnTopToSide, fromPointOnSideToBase)) { + //on side, including top rim or base rim + pointOnSide + } else { + //on top or base + // the full equation would be 'centerVector + slope * (height * 0.5f / Vector3.Magnitude(relativeUp))' + // 'relativeUp` is already a unit vector (magnitude of 1) + centerVector + slope * height * 0.5f + } + Point3D(target) + } + } +} + +object Cylinder { + /** + * An overloaded constructor where the 'relativeUp' of the cylinder is perpendicular to the xy-plane. + * @param p the point + * @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction + * @param height the distance between the "base" and the "top" + * @return + */ + def apply(p: Point3D, radius: Float, height: Float): Cylinder = Cylinder(p, Vector3(0,0,1), radius, height) + + /** + * An overloaded constructor where the origin point is expressed as a vector + * and the 'relativeUp' of the cylinder is perpendicular to the xy-plane. + * @param p the point + * @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction + * @param height the distance between the "base" and the "top" + * @return + */ + def apply(p: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point3D(p), Vector3(0,0,1), radius, height) + + /** + * An overloaded constructor the origin point is expressed as a vector. + * @param p the point + * @param v what the cylinder considers its "up" direction + * @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction + * @param height the distance between the "base" and the "top" + * @return + */ + def apply(p: Vector3, v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point3D(p), v, radius, height) +} diff --git a/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala b/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala index 0f759226..edc0408e 100644 --- a/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala +++ b/src/main/scala/net/psforever/objects/guid/actor/NumberPoolActor.scala @@ -44,7 +44,7 @@ class NumberPoolActor(pool: NumberPool) extends Actor { sender() ! NumberPoolActor.ReturnNumberResult(number, ex, id) case msg => - log.info(s"received an unexpected message - ${msg.toString}") + log.warn(s"Received an unexpected message - ${msg.toString}") } } diff --git a/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala b/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala index 47f5c996..06f0eca9 100644 --- a/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala +++ b/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala @@ -103,7 +103,7 @@ class UniqueNumberSystem(private val guid: NumberPoolHub, private val poolActors } } catch { case _: Exception => - log.info(s"$obj is already unregistered") + log.warn(s"$obj is already unregistered") callback ! Success(obj) } diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala index 1aeb5264..6a028144 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala @@ -118,10 +118,10 @@ trait AggravatedBehavior { ): AggravatedBehavior.Entry = { val cause = data.cause val aggravatedDamageInfo = DamageInteraction( - AggravatedDamage.burning(cause.resolution), target, + data.hitPos, cause, - data.hitPos + AggravatedDamage.burning(cause.resolution) ) val entry = AggravatedBehavior.Entry(id, effect, retime, aggravatedDamageInfo, powerOffset) entryIdToEntry += id -> entry diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala index 3e5150ce..1ad0c43d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala @@ -36,10 +36,7 @@ object DamageableMountable { ): Unit = { val zone = target.Zone val events = zone.AvatarEvents - val occupants = target.Seats.values.collect { - case seat if seat.isOccupied && seat.Occupant.get.isAlive => - seat.Occupant.get - } + val occupants = target.Seats.values.toSeq.flatMap { seat => seat.occupants.filter(_.isAlive) } ((cause.adversarial match { case Some(adversarial) => Some(adversarial.attacker) case None => None @@ -80,10 +77,10 @@ object DamageableMountable { val interaction = cause.interaction target.Seats.values .filter(seat => { - seat.isOccupied && seat.Occupant.get.isAlive + seat.isOccupied && seat.occupant.get.isAlive }) .foreach(seat => { - val tplayer = seat.Occupant.get + val tplayer = seat.occupant.get //tplayer.History(cause) tplayer.Actor ! Player.Die( DamageInteraction(interaction.resolution, SourceEntry(tplayer), interaction.cause, interaction.hitPos) 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 69401059..44dfbbc6 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -145,7 +145,7 @@ trait DamageableVehicle if (aggravated) { val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(totalDamage, Vector3.Zero)) obj.Seats.values - .collect { case seat if seat.Occupant.nonEmpty => seat.Occupant.get.Name } + .map { case seat if seat.occupant.nonEmpty => seat.occupant.get.Name } .foreach { channel => events ! VehicleServiceMessage(channel, msg) } @@ -158,7 +158,7 @@ trait DamageableVehicle } //alert cargo occupants to damage source obj.CargoHolds.values.foreach(hold => { - hold.Occupant match { + hold.occupant match { case Some(cargo) => cargo.Actor ! DamageableVehicle.Damage(cause, totalDamage) case None => ; @@ -198,7 +198,7 @@ trait DamageableVehicle DamageableMountable.DestructionAwareness(obj, cause) //cargo vehicles die with us obj.CargoHolds.values.foreach(hold => { - hold.Occupant match { + hold.occupant match { case Some(cargo) => cargo.Actor ! DamageableVehicle.Destruction(cause) case None => ; diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala index 4b840007..6ba61f48 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala @@ -73,7 +73,7 @@ trait DamageableWeaponTurret if (aggravated) { val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(damageToHealth, Vector3.Zero)) obj.Seats.values - .collect { case seat if seat.Occupant.nonEmpty => seat.Occupant.get.Name } + .collect { case seat if seat.occupant.nonEmpty => seat.occupant.get.Name } .foreach { channel => events ! VehicleServiceMessage(channel, msg) } diff --git a/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala b/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala index b10574bf..e6c34b84 100644 --- a/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala +++ b/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.doors import net.psforever.objects.Player +import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.structures.Amenity import net.psforever.packet.game.UseItemMessage @@ -65,6 +66,14 @@ object Door { */ final case class NoEvent() extends Exchange + type LockingMechanismLogic = (PlanetSideServerObject, Door) => Boolean + + final case class UpdateMechanism(mechanism: LockingMechanismLogic) extends Exchange + + case object Lock extends Exchange + + case object Unlock extends Exchange + /** * Overloaded constructor. * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields @@ -101,12 +110,25 @@ object Door { * @return the `Door` object */ def Constructor(pos: Vector3)(id: Int, context: ActorContext): Door = { - import akka.actor.Props import net.psforever.objects.GlobalDefinitions + Constructor(pos, GlobalDefinitions.door)(id, context) + } - val obj = Door(GlobalDefinitions.door) + /** + * Instantiate and configure a `Door` object that has knowledge of both its position and outwards-facing direction. + * The assumption is that this door will be paired with an IFF Lock, thus, has conditions for opening. + * @param pos the position of the door + * @param ddef the definition for this specific type of door + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `Door` object + */ + def Constructor(pos: Vector3, ddef: DoorDefinition)(id: Int, context: ActorContext): Door = { + import akka.actor.Props + + val obj = Door(ddef) obj.Position = pos - obj.Actor = context.actorOf(Props(classOf[DoorControl], obj), s"${GlobalDefinitions.door.Name}_$id") + obj.Actor = context.actorOf(Props(classOf[DoorControl], obj), s"${ddef.Name}_$id") obj } } diff --git a/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala b/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala index 10fa58b8..a13c1b55 100644 --- a/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala @@ -2,13 +2,12 @@ package net.psforever.objects.serverobject.doors import net.psforever.objects.Player -import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.locks.IFFLock -import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} +import net.psforever.objects.serverobject.structures.PoweredAmenityControl import net.psforever.services.Service import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} -import net.psforever.types.{PlanetSideEmpire, Vector3} /** * An `Actor` that handles messages being dispatched to a specific `Door`. @@ -18,44 +17,44 @@ class DoorControl(door: Door) extends PoweredAmenityControl with FactionAffinityBehavior.Check { def FactionObject: FactionAffinity = door + var isLocked: Boolean = false + var lockingMechanism: Door.LockingMechanismLogic = DoorControl.alwaysOpen val commonBehavior: Receive = checkBehavior + .orElse { + case Door.Lock => + isLocked = true + if (door.isOpen) { + val zone = door.Zone + door.Open = None + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.DoorSlamsShut(door)) + } + + case Door.Unlock => + isLocked = false + + case Door.UpdateMechanism(logic) => + lockingMechanism = logic + } def poweredStateLogic: Receive = commonBehavior .orElse { case CommonMessages.Use(player, _) => - val zone = door.Zone - val doorGUID = door.GUID - if ( - player.Faction == door.Faction || (zone.GUID(zone.map.doorToLock.getOrElse(doorGUID.guid, 0)) match { - case Some(lock: IFFLock) => - val owner = lock.Owner.asInstanceOf[Building] - val playerIsOnInside = Vector3.ScalarProjection(lock.Outwards, player.Position - door.Position) < 0f - /* - If an IFF lock exists and - the IFF lock faction doesn't match the current player and - one of the following conditions are met: - 1. player is on the inside of the door (determined by the lock orientation) - 2. lock is hacked - 3. facility capture terminal has been hacked - 4. base is neutral - ... open the door. - */ - playerIsOnInside || lock.HackedBy.isDefined || owner.CaptureTerminalIsHacked || lock.Faction == PlanetSideEmpire.NEUTRAL - case _ => true // no linked IFF lock, just try open the door - }) - ) { + if (lockingMechanism(player, door) && !isLocked) { openDoor(player) } + case IFFLock.DoorOpenResponse(target: Player) if !isLocked => + openDoor(target) + case _ => ; } def unpoweredStateLogic: Receive = { commonBehavior .orElse { - case CommonMessages.Use(player, _) => + case CommonMessages.Use(player, _) if !isLocked => //without power, the door opens freely openDoor(player) @@ -88,3 +87,7 @@ class DoorControl(door: Door) override def powerTurnOnCallback() : Unit = { } } + +object DoorControl { + def alwaysOpen(obj: PlanetSideServerObject, door: Door): Boolean = true +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala index 105acb1e..aa9d3274 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala @@ -2,9 +2,9 @@ package net.psforever.objects.serverobject.environment import enumeratum.{Enum, EnumEntry} -import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.vital.Vitality -import net.psforever.types.Vector3 +import net.psforever.types.{PlanetSideGUID, Vector3} /** * The representation of a feature of the game world that is not a formal game object, @@ -76,6 +76,17 @@ object EnvironmentAttribute extends Enum[EnvironmentTrait] { } } } + + case object GantryDenialField + extends EnvironmentTrait { + /** only interact with living player characters */ + def canInteractWith(obj: PlanetSideGameObject): Boolean = { + obj match { + case p: Player => p.isAlive + case _ => false + } + } + } } /** @@ -123,6 +134,14 @@ object Pool { Pool(attribute, DeepSquare(altitude, north, east, south, west)) } +final case class GantryDenialField( + obbasemesh: PlanetSideGUID, + mountPoint: Int, + collision: EnvironmentCollision + ) extends PieceOfEnvironment { + def attribute = EnvironmentAttribute.GantryDenialField +} + object PieceOfEnvironment { /** * Did the test point move into or leave the bounds of the represented environment since its previous test? diff --git a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala index 06cdc353..00166ff3 100644 --- a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala @@ -89,7 +89,7 @@ class GeneratorControl(gen: Generator) //TODO this only works with projectiles right now! val zone = gen.Zone gen.Health = 0 - super.DestructionAwareness(gen, gen.LastShot.get) + super.DestructionAwareness(gen, gen.LastDamage.get) GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Destroyed)) //kaboom zone.AvatarEvents ! AvatarServiceMessage( @@ -128,7 +128,7 @@ class GeneratorControl(gen: Generator) case GeneratorControl.Destabilized() => //if the generator is destabilized but has no ntu, it will not explode gen.Health = 0 - super.DestructionAwareness(gen, gen.LastShot.get) + super.DestructionAwareness(gen, gen.LastDamage.get) queuedExplosion.cancel() queuedExplosion = Default.Cancellable imminentExplosion = false diff --git a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala index 0614472c..c50e016a 100644 --- a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala +++ b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala @@ -104,7 +104,7 @@ object GenericHackables { def FinishHacking(target: PlanetSideServerObject with Hackable, user: Player, unk: Long)(): Unit = { import akka.pattern.ask import scala.concurrent.duration._ - log.info(s"Hacked a $target") + log.info(s"${user.Name} hacked a ${target.Definition.Name}") // Wait for the target actor to set the HackedBy property, otherwise LocalAction.HackTemporarily will not complete properly import scala.concurrent.ExecutionContext.Implicits.global val tplayer = user diff --git a/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala b/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala index ab13ad78..12953d83 100644 --- a/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala +++ b/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala @@ -1,6 +1,9 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.locks +import akka.actor.ActorRef +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.Amenity import net.psforever.packet.game.TriggeredSound @@ -48,6 +51,14 @@ class IFFLock(private val idef: IFFLockDefinition) extends Amenity with Hackable } object IFFLock { + final case class DoorOpenRequest(requestee: PlanetSideServerObject, door: Door, replyTo: ActorRef) + + final case class DoorOpenResponse(requestee: PlanetSideServerObject) + + def testLock(lock: IFFLock)(target: PlanetSideServerObject, door: Door): Boolean = { + lock.Actor ! IFFLock.DoorOpenRequest(target, door, door.Actor) + false + } /** * Overloaded constructor. diff --git a/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala b/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala index 59d665de..f2c9890e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala @@ -6,6 +6,8 @@ import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} +import net.psforever.objects.serverobject.structures.Building +import net.psforever.types.{PlanetSideEmpire, Vector3} /** * An `Actor` that handles messages being dispatched to a specific `IFFLock`. @@ -39,11 +41,32 @@ class IFFLockControl(lock: IFFLock) ) } else { val log = org.log4s.getLogger - log.warn("IFF lock is being hacked, but don't know how to handle this state:") + log.warn(s"IFF lock is being hacked by ${player.Faction}, but don't know how to handle this state:") log.warn(s"Lock - Faction=${lock.Faction}, HackedBy=${lock.HackedBy}") log.warn(s"Player - Faction=${player.Faction}") } + case IFFLock.DoorOpenRequest(target, door, replyTo) => + val owner = lock.Owner.asInstanceOf[Building] + /* + If one of the following conditions are met: + 1. target and door have same faction affinity + 2. lock or lock owner is neutral + 3. lock is hacked + 4. facility capture terminal (owner is a building) has been hacked + 5. requestee is on the inside of the door (determined by the lock orientation) + ... open the door. + */ + if ( + lock.Faction == target.Faction || + lock.Faction == PlanetSideEmpire.NEUTRAL || owner.Faction == PlanetSideEmpire.NEUTRAL || + lock.HackedBy.isDefined || + owner.CaptureTerminalIsHacked || + Vector3.ScalarProjection(lock.Outwards, target.Position - door.Position) < 0f + ) { + replyTo ! IFFLock.DoorOpenResponse(target) + } + case _ => ; //no default message } } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala new file mode 100644 index 00000000..be674fcb --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala @@ -0,0 +1,45 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.types.ExoSuitType + +trait MountRestriction[A] { + def test(target: A): Boolean +} + +case object MaxOnly extends MountRestriction[Player] { + def test(target: Player): Boolean = target.ExoSuit == ExoSuitType.MAX +} + +case object NoMax extends MountRestriction[Player] { + def test(target: Player): Boolean = target.ExoSuit != ExoSuitType.MAX +} + +case object NoReinforcedOrMax extends MountRestriction[Player] { + def test(target: Player): Boolean = target.ExoSuit != ExoSuitType.Reinforced && target.ExoSuit != ExoSuitType.MAX +} + +case object Unrestricted extends MountRestriction[Player] { + def test(target: Player): Boolean = true +} + +case object SmallCargo extends MountRestriction[Vehicle] { + def test(target: Vehicle): Boolean = { + target.Definition == GlobalDefinitions.ant || + target.Definition == GlobalDefinitions.quadassault || + target.Definition == GlobalDefinitions.quadstealth || + target.Definition == GlobalDefinitions.fury || + target.Definition == GlobalDefinitions.switchblade || + target.Definition == GlobalDefinitions.two_man_assault_buggy || + target.Definition == GlobalDefinitions.skyguard || + target.Definition == GlobalDefinitions.twomanheavybuggy || + target.Definition == GlobalDefinitions.twomanhoverbuggy || + target.Definition == GlobalDefinitions.threemanheavybuggy || + target.Definition == GlobalDefinitions.lightning + } +} + +case object LargeCargo extends MountRestriction[Vehicle] { + def test(target : Vehicle) : Boolean = !target.Definition.CanFly +} 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 abb6e791..16211210 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala @@ -3,7 +3,8 @@ package net.psforever.objects.serverobject.mount import akka.actor.ActorRef import net.psforever.objects.Player -import net.psforever.objects.vehicles.Seat + +import scala.annotation.tailrec /** * A `Trait` common to all game objects that permit players to @@ -12,38 +13,63 @@ import net.psforever.objects.vehicles.Seat * @see `Seat` */ trait Mountable { + protected var seats: Map[Int, Seat] = Map.empty /** - * Retrieve a mapping of each seat from its internal index. - * @return the mapping of index to seat + * Retrieve a mapping of each mount from its internal index. + * @return the mapping of index to mount */ - def Seats: Map[Int, Seat] + def Seats: Map[Int, Seat] = seats /** - * Given a seat's index position, retrieve the internal `Seat` object. - * @return the specific seat + * Given a mount's index position, retrieve the internal `Seat` object. + * @return the specific mount */ - def Seat(seatNum: Int): Option[Seat] + def Seat(seatNumber: Int): Option[Seat] = { + if (seatNumber >= 0 && seatNumber < seats.size) { + seats.get(seatNumber) + } else { + None + } + } /** - * Retrieve a mapping of each seat from its mount point index. - * @return the mapping of mount point to seat + * Retrieve a mapping of each mount from its mount point index. + * @return the mapping of mount point to mount */ - def MountPoints: Map[Int, Int] + def MountPoints: Map[Int, MountInfo] = Definition.MountPoints.toMap /** - * Given a mount point index, return the associated seat index. - * @param mount the mount point - * @return the seat index + * Given a mount point index, return the associated mount index. + * @param mountPoint the mount point + * @return the mount index */ - def GetSeatFromMountPoint(mount: Int): Option[Int] + def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = { + MountPoints.get(mountPoint) match { + case Some(mp) => Some(mp.seatIndex) + case _ => None + } + } /** * Given a player, determine if that player is seated. * @param user the player - * @return the seat index + * @return the mount index */ - def PassengerInSeat(user: Player): Option[Int] + def PassengerInSeat(user: Player): Option[Int] = recursivePassengerInSeat(seats.iterator, user) + + @tailrec private def recursivePassengerInSeat(iter: Iterator[(Int, Seat)], player: Player): Option[Int] = { + if (!iter.hasNext) { + None + } else { + val (seatNumber, seat) = iter.next() + if (seat.occupant.contains(player)) { + Some(seatNumber) + } else { + recursivePassengerInSeat(iter, player) + } + } + } /** * A reference to an `Actor` that governs the logic of the object to accept `Mountable` messages. @@ -53,6 +79,8 @@ trait Mountable { * @return the internal `ActorRef` */ def Actor: ActorRef //TODO can we enforce this desired association to MountableControl? + + def Definition: MountableDefinition } object Mountable { @@ -60,10 +88,15 @@ object Mountable { /** * Message used by the player to indicate the desire to board a `Mountable` object. * @param player the player who sent this request message + * @param mount_point the mount index + */ + final case class TryMount(player: Player, mount_point: Int) + + /** + * Message used by the player to indicate the desire to escape a `Mountable` object. + * @param player the player who sent this request message * @param seat_num the seat index */ - final case class TryMount(player: Player, seat_num: Int) - final case class TryDismount(player: Player, seat_num: Int) /** @@ -82,17 +115,17 @@ object Mountable { * Message sent in response to the player succeeding to access a `Mountable` object. * The player should be seated at the given index. * @param obj the `Mountable` object - * @param seat_num the seat index + * @param mount_point the mount index */ - final case class CanMount(obj: Mountable, seat_num: Int) extends Exchange + final case class CanMount(obj: Mountable, seat_number: Int, mount_point: Int) extends Exchange /** * Message sent in response to the player failing to access a `Mountable` object. * The player would have been be seated at the given index. * @param obj the `Mountable` object - * @param seat_num the seat index + * @param mount_point the mount index */ - final case class CanNotMount(obj: Mountable, seat_num: Int) extends Exchange + final case class CanNotMount(obj: Mountable, mount_point: Int) extends Exchange /** * Message sent in response to the player succeeding to disembark a `Mountable` object. @@ -100,7 +133,7 @@ object Mountable { * @param obj the `Mountable` object * @param seat_num the seat index */ - final case class CanDismount(obj: Mountable, seat_num: Int) extends Exchange + final case class CanDismount(obj: Mountable, seat_num: Int, mount_point: Int) extends Exchange /** * Message sent in response to the player failing to disembark a `Mountable` object. 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 f71d1f70..9d30a999 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala @@ -2,15 +2,33 @@ package net.psforever.objects.serverobject.mount import akka.actor.Actor -import net.psforever.objects.{Player, Vehicle} -import net.psforever.objects.entity.{Identifiable, WorldEntity} +import net.psforever.objects.Player +import net.psforever.objects.entity.WorldEntity import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.turret.WeaponTurret -import net.psforever.types.DriveState -object MountableBehavior { +import scala.collection.mutable + +trait MountableBehavior { + _ : Actor => + def MountableObject: PlanetSideServerObject with Mountable + + /** retain the mount point that was used by this occupant to mount */ + val usedMountPoint: mutable.HashMap[String, Int] = mutable.HashMap() + + def getUsedMountPoint(playerName: String, seatNumber: Int): Int = { + usedMountPoint + .remove(playerName) + .getOrElse { + MountableObject + .Definition + .MountPoints + .find { case (_, mp) => mp.seatIndex == seatNumber } match { + case Some((mount, _)) => mount + case None => -1 + } + } + } /** * The logic governing `Mountable` objects that use the `TryMount` message. @@ -18,54 +36,40 @@ object MountableBehavior { * @see `Seat` * @see `Mountable` */ - trait Mount { - _: Actor => - def MountableObject: PlanetSideServerObject with Mountable with FactionAffinity - - val mountBehavior: Receive = { - case Mountable.TryMount(user, seat_num) => - val obj = MountableObject - if (MountTest(MountableObject, seat_num, user)) { + val mountBehavior: Receive = { + case Mountable.TryMount(user, mount_point) => + val obj = MountableObject + obj.GetSeatFromMountPoint(mount_point) match { + case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) => user.VehicleSeated = obj.GUID - sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seat_num)) - } else { - sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) - } - } - - protected def MountTest(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = { - (player.Faction == obj.Faction || - (obj match { - case o: Hackable => o.HackedBy.isDefined - case _ => false - })) && - !obj.Destroyed && - (obj.Seats.get(seatNumber) match { - case Some(seat) => (seat.Occupant = player).contains(player) - case _ => false - }) - } + usedMountPoint.put(user.Name, mount_point) + sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point)) + case _ => + sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point)) + } } - trait TurretMount extends Mount { - _: Actor => + protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player + ): Boolean = { + (player.Faction == obj.Faction || + (obj match { + case o : Hackable => o.HackedBy.isDefined + case _ => false + })) && + !obj.Destroyed + } - override protected def MountTest( - obj: PlanetSideServerObject with Mountable, - seatNumber: Int, - player: Player - ): Boolean = { - obj match { - case wep: WeaponTurret => - (!wep.Definition.FactionLocked || player.Faction == obj.Faction) && - !obj.Destroyed && - (obj.Seats.get(seatNumber) match { - case Some(seat) => (seat.Occupant = player).contains(player) - case _ => false - }) - case _ => - super.MountTest(obj, seatNumber, player) - } + private def tryMount( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player + ): Boolean = { + obj.Seat(seatNumber) match { + case Some(seat) => seat.mount(player).contains(player) + case _ => false } } @@ -75,29 +79,41 @@ object MountableBehavior { * @see `Seat` * @see `Mountable` */ - trait Dismount { - this: Actor => + val dismountBehavior: Receive = { + case Mountable.TryDismount(user, seat_number) => + val obj = MountableObject + if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user)) { + user.VehicleSeated = None + sender() ! Mountable.MountMessages( + user, + Mountable.CanDismount(obj, seat_number, getUsedMountPoint(user.Name, seat_number)) + ) + } + else { + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(obj, seat_number)) + } + } - def MountableObject: Mountable with Identifiable with WorldEntity with FactionAffinity + protected def dismountTest( + obj: Mountable with WorldEntity, + seatNumber: Int, + user: Player + ): Boolean = { + obj.PassengerInSeat(user).contains(seatNumber) && + (obj.Seats.get(seatNumber) match { + case Some(seat) => seat.bailable || !obj.isMoving(test = 1) + case _ => false + }) + } - val dismountBehavior: Receive = { - case Mountable.TryDismount(user, seat_num) => - val obj = MountableObject - obj.Seat(seat_num) match { - case Some(seat) => - if ( - seat.Bailable || !obj.isMoving(1) || (obj - .isInstanceOf[Vehicle] && obj.asInstanceOf[Vehicle].DeploymentState == DriveState.Deployed) - ) { - seat.Occupant = None - user.VehicleSeated = None - sender() ! Mountable.MountMessages(user, Mountable.CanDismount(obj, seat_num)) - } else { - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(obj, seat_num)) - } - case None => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(obj, seat_num)) - } + private def tryDismount( + obj: Mountable, + seatNumber: Int, + user: Player + ): Boolean = { + obj.Seats.get(seatNumber) match { + case Some(seat) => seat.unmount(user).isEmpty + case _ => false } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableDefinition.scala new file mode 100644 index 00000000..e72641f3 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableDefinition.scala @@ -0,0 +1,23 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.types.Vector3 + +import scala.collection.mutable + +final case class MountInfo(seatIndex: Int, positionOffset: Vector3) + +object MountInfo { + def apply(seatIndex: Int): MountInfo = MountInfo(seatIndex, Vector3.Zero) +} + +trait MountableDefinition { + /* key - mount index, value - mount object */ + private val seats: mutable.HashMap[Int, SeatDefinition] = mutable.HashMap[Int, SeatDefinition]() + /* key - entry point index, value - mount index */ + private val mountPoints: mutable.HashMap[Int, MountInfo] = mutable.HashMap() + + def Seats: mutable.HashMap[Int, SeatDefinition] = seats + + def MountPoints: mutable.HashMap[Int, MountInfo] = mountPoints +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala new file mode 100644 index 00000000..25c046ba --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpace.scala @@ -0,0 +1,104 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +trait MountableSpace[A] { + private var _occupant: Option[A] = None + + /** + * A single mounted entity. + * @return one mounted entity at most, or `None` + */ + def occupant: Option[A] = _occupant + + /** + * A collection of any mounted entity. + * Useful for compiling all seated users using `flatMap`. + * @return all mounted entities + */ + def occupants: List[A] = _occupant.toList + + /** + * Is anything be seated? + * Do not use this method as a test for "availability". + */ + def isOccupied: Boolean = _occupant.nonEmpty + + /** + * Can something be mounted? + * Use this method as a test for "availability". + */ + def canBeOccupied: Boolean = _occupant.isEmpty + + /** + * Is this specific entity currently mounted? + */ + def isOccupiedBy(target: A): Boolean = _occupant.contains(target) + + /** + * Is this specific entity allowed to be mounted in this space? + * Utiltizes restriction tests, but not "availability" tests. + * @see `MountableDefinition[A].restriction` + */ + def canBeOccupiedBy(target: A): Boolean = definition.restriction.test(target) + + /** + * Attempt to mount the target entity in this space. + */ + def mount(target: A): Option[A] = mount(Some(target)) + + /** + * Attempt to mount the target entity in this space. + */ + def mount(target: Option[A]): Option[A] = { + target match { + case Some(p) if testToMount(p) => + _occupant = target + target + case _ => + occupant + } + } + + /** + * Tests whether the target is allowed to be mounted. + * @see `MountableSpace[A].canBeOccupiedBy(A)` + */ + protected def testToMount(target: A): Boolean = canBeOccupied && canBeOccupiedBy(target) + + /** + * Attempt to dismount the target entity from this space. + */ + def unmount(target: A): Option[A] = unmount(Some(target)) + + /** + * Attempt to dismount the target entity from this space. + */ + def unmount(target: Option[A]): Option[A] = { + target match { + case Some(p) if testToUnmount(p) => + _occupant = None + None + case _ => + occupant + } + } + + /** + * Tests whether the target is capable of being unmounted from this place. + * @see `MountableSpace[A].isOccupiedBy(A)` + */ + protected def testToUnmount(target: A): Boolean = isOccupiedBy(target) + + /** + * Does this mountable space count as being "bailable", + * a condition whereupon it can be unmounted under duress? + * The conditions of the duress do not matter at the moment; + * this is only a test of possibility. + */ + def bailable: Boolean = definition.bailable + + /** + * The information that establishes the underlying characteristics of this mountable space. + */ + def definition: MountableSpaceDefinition[A] +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpaceDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpaceDefinition.scala new file mode 100644 index 00000000..2b199d0f --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableSpaceDefinition.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.objects.definition.BasicDefinition + +trait MountableSpaceDefinition[A] + extends BasicDefinition { + def occupancy: Int + + def restriction: MountRestriction[A] + + def bailable: Boolean +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/Seat.scala b/src/main/scala/net/psforever/objects/serverobject/mount/Seat.scala new file mode 100644 index 00000000..23b5239b --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/Seat.scala @@ -0,0 +1,10 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.objects.Player + +class Seat(private val sdef: SeatDefinition) extends MountableSpace[Player] { + override protected def testToMount(target: Player): Boolean = target.VehicleSeated.isEmpty && super.testToMount(target) + + def definition: SeatDefinition = sdef +} diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/SeatDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/mount/SeatDefinition.scala new file mode 100644 index 00000000..dfcd9ed8 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/SeatDefinition.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.mount + +import net.psforever.objects.Player + +class SeatDefinition extends MountableSpaceDefinition[Player] { + Name = "mount" + var occupancy: Int = 1 + + var restriction: MountRestriction[Player] = NoMax + + var bailable: Boolean = false +} diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala index 5b5ac173..8d3dfdb9 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala @@ -87,7 +87,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) /* When the vehicle is spawned and added to the pad, it will "occupy" the pad and block it from further action. - Normally, the player who wanted to spawn the vehicle will be automatically put into the driver seat. + Normally, the player who wanted to spawn the vehicle will be automatically put into the driver mount. If this is blocked, the vehicle will idle on the pad and must be moved far enough away from the point of origin. During this time, a periodic message about the spawn pad being blocked will be broadcast to all current customers in the order queue. @@ -220,8 +220,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) */ def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnControl.Order]): Unit = { val user = blockedOrder.vehicle - .Seats(0) - .Occupant + .Seats(0).occupant .orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner)) .orElse(pad.Zone.GUID(blockedOrder.DriverGUID)) val relevantRecipients = user match { diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala index 61db390d..f3c9ceea 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala @@ -75,7 +75,7 @@ object VehicleSpawnPad { final case class ResetSpawnPad(pad: VehicleSpawnPad) /** - * Message that acts as callback to the driver that the process of sitting in the driver seat will be initiated soon. + * Message that acts as callback to the driver that the process of sitting in the driver mount will be initiated soon. * This information should only be communicated to the driver's client only. * @param driver_name the person who will drive the vehicle * @param vehicle the vehicle being spawned @@ -84,7 +84,7 @@ object VehicleSpawnPad { final case class StartPlayerSeatedInVehicle(driver_name: String, vehicle: Vehicle, pad: VehicleSpawnPad) /** - * Message that acts as callback to the driver that the process of sitting in the driver seat should be finished. + * Message that acts as callback to the driver that the process of sitting in the driver mount should be finished. * This information should only be communicated to the driver's client only. * @param driver_name the person who will drive the vehicle * @param vehicle the vehicle being spawned diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala index 5b3c39b0..2667d785 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala @@ -14,7 +14,7 @@ import scala.concurrent.duration._ *
* This object is the first link in the process chain that spawns the ordered vehicle. * It is devoted to causing the prospective driver to become hidden during the first part of the process - * with the goal of appearing to be "teleported" into the driver seat. + * with the goal of appearing to be "teleported" into the driver mount. * It has failure cases should the driver be in an incorrect state. * @param pad the `VehicleSpawnPad` object being governed */ diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala index 98e24cd0..d589bf0d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala @@ -23,7 +23,7 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont def LogId = "-lifter" val seatDriver = - context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-seat") + context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-mount") def receive: Receive = { case order @ VehicleSpawnControl.Order(_, vehicle) => diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala index 69916530..4235cf55 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -13,11 +13,11 @@ import scala.concurrent.duration._ * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
*
- * This object forces the prospective driver to take the driver seat. + * This object forces the prospective driver to take the driver mount. * Multiple separate but sequentially significant steps occur within the scope of this object. * First, this step waits for the vehicle to be completely ready to accept the driver. - * Second, this step triggers the player to actually be moved into the driver seat. - * Finally, this step waits until the driver is properly in the driver seat. + * Second, this step triggers the player to actually be moved into the driver mount. + * Finally, this step waits until the driver is properly in the driver mount. * It has failure cases should the driver or the vehicle be in an incorrect state. * @see `ZonePopulationActor` * @param pad the `VehicleSpawnPad` object being governed diff --git a/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala b/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala index f38201cf..4930bff5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala @@ -24,7 +24,7 @@ class PainboxControl(painbox: Painbox) extends PoweredAmenityControl { if (painbox.Owner.Continent.matches("c[0-9]")) { //are we in a safe zone? // todo: handle non-radius painboxes in caverns properly - log.info(s"Skipping initialization of ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position}") + log.debug(s"Skipping initialization of ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position}") disabled = true } else { if (painbox.Definition.HasNearestDoorDependency) { diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index 0d03f5ab..fb873f84 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -99,7 +99,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) // Only send updated capacitor display value to all clients if it has actually changed if (resourceSilo.CapacitorDisplay != siloDisplayBeforeChange) { log.trace( - s"Silo ${resourceSilo.GUID} NTU bar level has changed from $siloDisplayBeforeChange to ${resourceSilo.CapacitorDisplay}" + s"UpdateChargeLevel: silo ${resourceSilo.GUID} NTU bar level has changed from $siloDisplayBeforeChange to ${resourceSilo.CapacitorDisplay}" ) zone.AvatarEvents ! AvatarServiceMessage( zone.id, diff --git a/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttle.scala b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttle.scala new file mode 100644 index 00000000..3725fbb5 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttle.scala @@ -0,0 +1,85 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.shuttle + +import net.psforever.objects.Vehicle +import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.serverobject.mount.Seat +import net.psforever.objects.vehicles.AccessPermissionGroup + +/** + * The high altitude rapid transport (HART) orbital shuttle is a special vehicle + * that is paired with a formal building `Amenity` called the orbital shuttle pad (`obbasemesh`) + * and is only found in the HART buildings (`orbital_building_`{faction}) of a given faction's sanctuary zone.
+ *
+ * It has no pilot and can not be piloted. + * Unlike other vehicles, it has the potential for a very sizeable passenger capacity. + * Despite this, it is intended to start with a single mount. + * That one mount should contain the information needed to create a given number of spontaneous passenger mount points. + * Whenever a valid user would try to find a mount, and there are no mounts available, + * and the total number of created mounts has not yet exceeded the limits set by the original mount's designation, + * then a completely new mount can be created and the user attached. + * All spontaneous mounts have the same properties as the original mount. + * @param sdef the vehicle's definition entry + */ +class OrbitalShuttle(sdef: VehicleDefinition) extends Vehicle(sdef) { + /** + * Either locate a place for a passenger to mount, + * or designate a spontaneous mount point to handle a new passenger. + * The only time there is no more space is when the no new spontaneous seats can be counted. + * @param mountPoint the mount point + * @return the mount index + */ + override def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = { + super.GetSeatFromMountPoint(mountPoint) match { + case Some(0) => + seats.find { case (_, seat) => !seat.isOccupied } match { + case Some((seatNumber, _)) => Some(seatNumber) + case None if seats.size < seats(0).definition.occupancy => Some(seats.size) + case _ => None + } + case _ => + None + } + } + + /** + * Either locate a place for a passenger to mount, + * or create a spontaneous mount point to handle the new passenger. + * The only time there is no more space is when the no new spontaneous seats can be created. + * This new seat becomes "real" and will continue to exist after being dismounted. + * @param seatNumber the index of a mount point + * @return the specific mount + */ + override def Seat(seatNumber: Int): Option[Seat] = { + val sdef = seats(0).definition + super.Seat(seatNumber) match { + case out @ Some(_) => + out + case None if seatNumber == seats.size && seatNumber < sdef.occupancy => + val newSeat = new Seat(sdef) + seats = seats ++ Map(seatNumber -> newSeat) + Some(newSeat) + case _ => + None + } + } + + /** + * All players mounted in the shuttle are passengers only. No driver. No gunners. + * Even if it does not exist yet, as long as it has the potential to be created, + * discuss the next seat that would be created as if it already exists. + * @param seatNumber the index of a mount point + * @return `Passenger` permissions + */ + override def SeatPermissionGroup(seatNumber : Int) : Option[AccessPermissionGroup.Value] = { + Seats.get(seatNumber) match { + case Some(_) => + Some(AccessPermissionGroup.Passenger) + case None + if seats.size == seatNumber && Seats.values.exists { _.definition.occupancy > seats.size } => + Some(AccessPermissionGroup.Passenger) + case _ => + None + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePad.scala b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePad.scala new file mode 100644 index 00000000..79d4e5e8 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePad.scala @@ -0,0 +1,71 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.shuttle + +import akka.actor.ActorRef +import net.psforever.objects.Vehicle +import net.psforever.objects.serverobject.structures.{Amenity, AmenityDefinition} +import net.psforever.types.PlanetSideGUID + +/** + * The orbital shuttle pad which is the primary component of the high altitude rapid transport (HART) system.
+ *
+ * The orbital shuttle pad is a type of flat called an `obbasemesh`. + * The shuttle component of the HART casually perches on top of the pad and + * adjusts its states to control animation and passenger access. + * The shuttle that is visible to the player and flies in and out of the zone is actually a hologram + * of the real shuttle that is an invisible, intangible vehicle + * forever stationary on top of the building. + * @param spDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ +class OrbitalShuttlePad(spDef: AmenityDefinition) extends Amenity { + private var _shuttle: Option[PlanetSideGUID] = None + + def shuttle: Option[PlanetSideGUID] = _shuttle + + def shuttle_=(orbitalShuttle: Vehicle): Option[PlanetSideGUID] = { + _shuttle = _shuttle.orElse(Some(orbitalShuttle.GUID)) + _shuttle + } + + def Definition: AmenityDefinition = spDef +} + +object OrbitalShuttlePad { + final case class GetShuttle(giveTo: ActorRef) + + final case class GiveShuttle(shuttle: Vehicle) + + /** + * Overloaded constructor. + * @param spDef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + * @return an `OrbitalShuttlePad` object + */ + def apply(spDef: AmenityDefinition): OrbitalShuttlePad = { + new OrbitalShuttlePad(spDef) + } + + import akka.actor.ActorContext + import net.psforever.types.Vector3 + + /** + * Instantiate and configure an `OrbitalShuttlePad` object + * @param pdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + * @param pos the position (used to determine spawn point) + * @param orient the orientation (used to indicate spawn direction) + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `OrbitalShuttlePad` object + */ + def Constructor(pos: Vector3, pdef: AmenityDefinition, orient: Vector3)( + id: Int, + context: ActorContext + ): OrbitalShuttlePad = { + import akka.actor.Props + + val obj = OrbitalShuttlePad(pdef) + obj.Position = pos + obj.Orientation = orient + obj.Actor = context.actorOf(Props(classOf[OrbitalShuttlePadControl], obj), s"${obj.Definition.Name}_$id") + obj + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePadControl.scala b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePadControl.scala new file mode 100644 index 00000000..83f218cb --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/shuttle/OrbitalShuttlePadControl.scala @@ -0,0 +1,203 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.shuttle + +import akka.actor.{Actor, ActorRef} +import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} +import net.psforever.objects.{Player, Vehicle} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.ChatMsg +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.hart.{HartTimer, HartTimerActions} +import net.psforever.services.{Service, ServiceManager} +import net.psforever.types.ChatMessageType + +import scala.util.Success + +/** + * An `Actor` that handles messages being dispatched to a specific `OrbitalShuttlePad`.
+ *
+ * For the purposes of maintaining a close relationship + * with the rest of the high altitude rapid transport (HART) system's components, + * this control agency also locally creates the vehicle that will the shuttle when it starts up. + * The shuttle should be treated like a supporting object to the zone + * that exists within the normal vehicle pipeline. + * @see `ShuttleState` + * @see `ShuttleTimer` + * @see `HartService` + * @param pad the `OrbitalShuttlePad` object being governed + */ +class OrbitalShuttlePadControl(pad: OrbitalShuttlePad) extends Actor { + /** the doors that allow would be passengers to access the shuttle boarding gantries + * (actually, a hallway with a teleport); + * the target doors are of a specific type that flag their purpose - "gr_door_mb_orb" + */ + var managedDoors: List[Door] = Nil + var shuttle: Vehicle = _ + + def receive: Receive = startUp + + /** the HART system is active and ready to handle state changes */ + val taxiing: Receive = { + case OrbitalShuttlePad.GetShuttle(to) => + to ! OrbitalShuttlePad.GiveShuttle(shuttle) + + case HartTimer.LockDoors => + managedDoors.foreach { door => + door.Actor ! Door.UpdateMechanism(OrbitalShuttlePadControl.lockedWaitingForShuttle) + val zone = pad.Zone + if(door.isOpen) { + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.DoorSlamsShut(door)) + } + } + + case HartTimer.UnlockDoors => + managedDoors.foreach { _.Actor ! Door.UpdateMechanism(OrbitalShuttlePadControl.shuttleIsBoarding) } + + case HartTimer.ShuttleDocked(forChannel) => + HartTimerActions.ShuttleDocked(pad, shuttle, forChannel) + + case HartTimer.ShuttleFreeFromDock(forChannel) => + HartTimerActions.ShuttleFreeFromDock(pad, shuttle, forChannel) + + case HartTimer.ShuttleStateUpdate(forChannel, state) => + HartTimerActions.ShuttleStateUpdate(pad, shuttle, forChannel, state) + + case _ => ; + } + + /** wire the pad and shuttle into a zone-scoped service handler */ + val shuttleTime: Receive = { + case Zone.Vehicle.HasSpawned(_, newShuttle: OrbitalShuttle) => + shuttle = newShuttle + pad.shuttle = newShuttle + pad.Owner.Amenities = new ShuttleAmenity(newShuttle) + ServiceManager.serviceManager ! ServiceManager.Lookup("hart") + + case ServiceManager.LookupResult(_, timer) => + timer ! HartTimer.PairWith(pad.Zone, pad.GUID, shuttle.GUID, self) + context.become(taxiing) + + case Zone.Vehicle.CanNotSpawn(zone, _, reason) => + org.log4s + .getLogger("OrbitalShuttle") + .error(s"shuttle for pad#${pad.Owner.GUID.guid} in zone ${zone.id} did not spawn - $reason") + //seal doors + managedDoors.foreach { _.Actor ! Door.UpdateMechanism(OrbitalShuttlePadControl.lockedWaitingForShuttle) } + + case msg: HartTimer.Command => + self.forward(msg) //delay? + + case _ => ; + } + + /** collect all of the doors that will be controlled by the HART system; + * set up the shuttle information based on the pad to which it belongs; + * register and add the shuttle as a common vehicle of the said zone + */ + val startUp: Receive = { + case Service.Startup() => + import net.psforever.types.Vector3 + import net.psforever.types.Vector3.DistanceSquared + import net.psforever.objects.GlobalDefinitions._ + val position = pad.Position + val zone = pad.Zone + //collect managed doors + managedDoors = pad.Owner.Amenities + .collect { case d: Door if d.Definition == gr_door_mb_orb => d } + .sortBy { o => DistanceSquared(position, o.Position) } + .take(8) + //create shuttle + val newShuttle = new OrbitalShuttle(orbital_shuttle) + newShuttle.Position = position + Vector3(0, -8.25f, 0).Rz(pad.Orientation.z) //magic offset number + newShuttle.Orientation = pad.Orientation + newShuttle.Faction = pad.Faction + zone.tasks ! OrbitalShuttlePadControl.registerShuttle(zone, newShuttle, self) + context.become(shuttleTime) + + case _ => ; + } +} + +object OrbitalShuttlePadControl { + /** + * Register the shuttle as a common vehicle in a zone. + * @param zone the zone the shuttle and the pad will occupy + * @param shuttle the vehicle that will be the shuttle + * @param ref a reference to the control agency for the orbital shuttle pad + * @return a `TaskResolver.GiveTask` object + */ + def registerShuttle(zone: Zone, shuttle: Vehicle, ref: ActorRef): TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localZone = zone + private val localShuttle = shuttle + private val localSelf = ref + + override def Description: String = s"register an orbital shuttle" + + override def isComplete : Task.Resolution.Value = if (localShuttle.HasGUID) { + Task.Resolution.Success + } else { + Task.Resolution.Incomplete + } + + def Execute(resolver : ActorRef) : Unit = { + localZone.Transport.tell(Zone.Vehicle.Spawn(localShuttle), localSelf) + resolver ! Success(true) + } + + override def onFailure(ex : Throwable) : Unit = { + super.onFailure(ex) + localSelf ! Zone.Vehicle.CanNotSpawn(localZone, localShuttle, ex.getMessage) + } + }, List(GUIDTask.RegisterVehicle(shuttle)(zone.GUID)) + ) + } + + /** + * Logic for door mechanism that allows the shuttle entryway to be opened. + * Only opens for users with proper faction affinity. + * @param obj what attempted to open the door + * @param door the door + * @return `true`, if the user is the accepted by the door; + * `false`, otherwise + */ + def shuttleIsBoarding(obj: PlanetSideServerObject, door: Door): Boolean = { + obj.Faction == door.Faction + } + + /** + * Logic for door mechanism that keeps select doors shut when the shuttle is not ready for boarding. + * A message flashes onscreen to explain this reason. + * The message will not flash if the door has no expectation of ever opening for a user. + * @see `AvatarAction.SendResponse` + * @see `AvatarServiceMessage` + * @see `ChatMessageType` + * @see `ChatMsg` + * @see `Player` + * @see `Service` + * @see `Zone.AvatarEvents` + * @param obj what attempted to open the door + * @param door the door + * @return `false`, as the door can not be opened in this state + */ + def lockedWaitingForShuttle(obj: PlanetSideServerObject, door: Door): Boolean = { + val zone = door.Zone + obj match { + case p: Player if p.Faction == door.Faction => + zone.AvatarEvents ! AvatarServiceMessage( + p.Name, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + ChatMsg(ChatMessageType.UNK_225, false, "", "@DoorWillOpenWhenShuttleReturns", None) + ) + ) + p.Name + case _ => ; + } + false + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala b/src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala new file mode 100644 index 00000000..e7fdd8fb --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala @@ -0,0 +1,42 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.shuttle + +import akka.actor.ActorRef +import net.psforever.objects.serverobject.structures.{Amenity, AmenityDefinition} +import net.psforever.types.PlanetSideGUID + +/** + * A pseudo-`Amenity` of the high-altitude rapid transport (HART) building + * whose sole purpose is to allow the HART orbital shuttle to be initialized + * as if it were a normal `Amenity`-level feature of the building. + * This should not be considered an actual game object as defined by the game. + * It should resemble the orbital shuttle that it wraps in most important measurable ways. + * @see `OrbitalShuttleControl` + * @throws `AssertionError` if the vehicle is not a `OrbitalShuttle` + * @param shuttle the shuttle + */ +class ShuttleAmenity(shuttle: OrbitalShuttle) extends Amenity { + override def GUID = shuttle.GUID + + override def GUID_=(guid: PlanetSideGUID) = GUID + + override def DamageModel = shuttle.DamageModel + + override def Actor = shuttle.Actor + + override def Actor_=(control: ActorRef) = Actor + + override def Health = shuttle.Health + + override def Faction = shuttle.Faction + + def Definition = ShuttleAmenity.definition +} + +object ShuttleAmenity { + final val definition = new AmenityDefinition(net.psforever.packet.game.objectcreate.ObjectClass.orbital_shuttle) { + Name = "orbital_shuttle_fake" + Damageable = false + Repairable = false + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminals.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminals.scala new file mode 100644 index 00000000..cc638369 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminals.scala @@ -0,0 +1,57 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.terminals + +import net.psforever.objects.Player +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal +import net.psforever.services.local.{LocalAction, LocalServiceMessage} + +import scala.util.{Failure, Success} + +object CaptureTerminals { + private val log = org.log4s.getLogger("CaptureTerminals") + + /** + * The process of hacking an object is completed. + * Pass the message onto the hackable object and onto the local events system. + * @param target the `Hackable` object that has been hacked + * @param unk na; + * used by `HackMessage` as `unk5` + * @see `HackMessage` + */ + //TODO add params here depending on which params in HackMessage are important + def FinishHackingCaptureConsole(target: CaptureTerminal, hackingPlayer: Player, unk: Long)(): Unit = { + import akka.pattern.ask + import scala.concurrent.duration._ + log.info(s"${hackingPlayer.toString} hacked a ${target.Definition.Name}") + // Wait for the target actor to set the HackedBy property + import scala.concurrent.ExecutionContext.Implicits.global + ask(target.Actor, CommonMessages.Hack(hackingPlayer, target))(1 second).mapTo[Boolean].onComplete { + case Success(_) => + target.Zone.LocalEvents ! LocalServiceMessage( + target.Zone.id, + LocalAction.TriggerSound(hackingPlayer.GUID, target.HackSound, hackingPlayer.Position, 30, 0.49803925f) + ) + val isResecured = hackingPlayer.Faction == target.Faction + if (isResecured) { + // Resecure the CC + target.Zone.LocalEvents ! LocalServiceMessage( + target.Zone.id, + LocalAction.ResecureCaptureTerminal( + target + ) + ) + } + else { + // Start the CC hack timer + target.Zone.LocalEvents ! LocalServiceMessage( + target.Zone.id, + LocalAction.StartCaptureTerminalHack( + target + ) + ) + } + case Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}") + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala index 985f122e..231db45f 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala @@ -21,11 +21,12 @@ trait CaptureTerminalAwareBehavior { if (CaptureTerminalAwareObject.isInstanceOf[Mountable]) { CaptureTerminalAwareObject.asInstanceOf[Mountable].Seats.filter(x => x._2.isOccupied).foreach(x => { val (seat_num, seat) = x + val user = seat.occupant.get CaptureTerminalAwareObject.Zone.VehicleEvents ! VehicleServiceMessage( CaptureTerminalAwareObject.Zone.id, - VehicleAction.KickPassenger(seat.Occupant.get.GUID, seat_num, true, CaptureTerminalAwareObject.GUID)) - - seat.Occupant = None + VehicleAction.KickPassenger(user.GUID, seat_num, true, CaptureTerminalAwareObject.GUID) + ) + seat.unmount(user) }) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala index 9a37c1a3..06109a34 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala @@ -1,14 +1,9 @@ package net.psforever.objects.serverobject.terminals.capture -import net.psforever.actors.zone.BuildingActor import net.psforever.objects.Player import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.packet.game.PlanetsideAttributeEnum import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import net.psforever.types.PlanetSideEmpire -import java.util.concurrent.TimeUnit import scala.util.{Failure, Success} object CaptureTerminals { @@ -28,7 +23,7 @@ object CaptureTerminals { import akka.pattern.ask import scala.concurrent.duration._ - log.info(s"${hackingPlayer.toString} Hacked a ${target.toString}") + log.info(s"${hackingPlayer.toString} hacked a ${target.Definition.Name}") // Wait for the target actor to set the HackedBy property import scala.concurrent.ExecutionContext.Implicits.global ask(target.Actor, CommonMessages.Hack(hackingPlayer, target))(1 second).mapTo[Boolean].onComplete { diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala index 063f1560..1622650b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala @@ -1,12 +1,10 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals.implant -import net.psforever.objects.Player import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.mount.{Mountable, Seat} import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware -import net.psforever.objects.vehicles.Seat import net.psforever.packet.game.TriggeredSound import net.psforever.types.Vector3 @@ -20,28 +18,12 @@ class ImplantTerminalMech(private val idef: ImplantTerminalMechDefinition) with Mountable with Hackable with CaptureTerminalAware { - private val seats: Map[Int, Seat] = Map(0 -> new Seat(idef.Seats(0))) + seats = Map(0 -> new Seat(idef.Seats.head._2)) HackSound = TriggeredSound.HackTerminal HackEffectDuration = Array(0, 30, 60, 90) HackDuration = Array(0, 10, 5, 3) - def Seats: Map[Int, Seat] = seats - - def Seat(seatNum: Int): Option[Seat] = seats.get(seatNum) - - def MountPoints: Map[Int, Int] = idef.MountPoints - - def GetSeatFromMountPoint(mount: Int): Option[Int] = idef.MountPoints.get(mount) - - def PassengerInSeat(user: Player): Option[Int] = { - if (seats(0).Occupant.contains(user)) { - Some(0) - } else { - None - } - } - def Definition: ImplantTerminalMechDefinition = idef } diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala index d2b386b3..4ab3462e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala @@ -21,8 +21,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} class ImplantTerminalMechControl(mech: ImplantTerminalMech) extends PoweredAmenityControl with FactionAffinityBehavior.Check - with MountableBehavior.Mount - with MountableBehavior.Dismount + with MountableBehavior with HackableBehavior.GenericHackable with DamageableEntity with RepairableEntity @@ -68,11 +67,11 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) case _ => ; } - override protected def MountTest( - obj: PlanetSideServerObject with Mountable, - seatNumber: Int, - player: Player - ): Boolean = { + override protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player + ): Boolean = { val zone = obj.Zone zone.map.terminalToInterface.get(obj.GUID.guid) match { case Some(interface_guid) => @@ -80,7 +79,7 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) case Some(interface) => !interface.Destroyed case None => false }) && - super.MountTest(obj, seatNumber, player) + super.mountTest(obj, seatNumber, player) case None => false } @@ -122,9 +121,9 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) val zoneId = zone.id val events = zone.VehicleEvents mech.Seats.values.foreach(seat => - seat.Occupant match { + seat.occupant match { case Some(player) => - seat.Occupant = None + seat.unmount(player) player.VehicleSeated = None if (player.HasGUID) { events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechDefinition.scala index 0b351212..7720b83d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechDefinition.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals.implant -import net.psforever.objects.definition.SeatDefinition +import net.psforever.objects.serverobject.mount.{MountInfo, MountableDefinition, SeatDefinition, Unrestricted} import net.psforever.objects.serverobject.structures.AmenityDefinition /** @@ -9,14 +9,15 @@ import net.psforever.objects.serverobject.structures.AmenityDefinition * Implant terminals are composed of two components. * This `Definition` constructs the visible mechanical tube component that can be mounted. */ -class ImplantTerminalMechDefinition extends AmenityDefinition(410) { - /* key - seat index, value - seat object */ - private val seats: Map[Int, SeatDefinition] = Map(0 -> new SeatDefinition) - /* key - entry point index, value - seat index */ - private val mountPoints: Map[Int, Int] = Map(1 -> 0) +class ImplantTerminalMechDefinition + extends AmenityDefinition(410) + with MountableDefinition { Name = "implant_terminal_mech" - def Seats: Map[Int, SeatDefinition] = seats - - def MountPoints: Map[Int, Int] = mountPoints + /* key - mount index, value - mount object */ + Seats += 0 -> new SeatDefinition() { + restriction = Unrestricted + } + /* key - entry point index, value - mount index */ + MountPoints += 1 -> MountInfo(0) } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala index cc4de5eb..ae056a58 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala @@ -13,8 +13,6 @@ class FacilityTurret(tDef: FacilityTurretDefinition) with CaptureTerminalAware { WeaponTurret.LoadDefinition(this) - def MountPoints: Map[Int, Int] = Definition.MountPoints.toMap - def Definition: FacilityTurretDefinition = tDef } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index 53a27e49..d735fe68 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -3,8 +3,8 @@ package net.psforever.objects.serverobject.turret import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool} import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons} -import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.mount.MountableBehavior +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret} import net.psforever.objects.serverobject.hackable.GenericHackables @@ -31,8 +31,7 @@ import scala.concurrent.duration._ class FacilityTurretControl(turret: FacilityTurret) extends PoweredAmenityControl with FactionAffinityBehavior.Check - with MountableBehavior.TurretMount - with MountableBehavior.Dismount + with MountableBehavior with DamageableWeaponTurret with RepairableWeaponTurret with AmenityAutoRepair @@ -74,7 +73,7 @@ class FacilityTurretControl(turret: FacilityTurret) item.Magazine > 0 && turret.Seats.values.forall(!_.isOccupied) => TurretUpgrade.values.find(_.id == upgradeValue) match { case Some(upgrade) - if turret.Upgrade != upgrade && turret.Definition.Weapons.values + if turret.Upgrade != upgrade && turret.Definition.WeaponPaths.values .flatMap(_.keySet) .exists(_ == upgrade) => sender() ! CommonMessages.Progress( @@ -103,7 +102,7 @@ class FacilityTurretControl(turret: FacilityTurret) if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) { weapon.Magazine += 1 val seat = turret.Seat(0).get - seat.Occupant match { + seat.occupant match { case Some(player: Player) => turret.Zone.LocalEvents ! LocalServiceMessage( turret.Zone.id, @@ -126,6 +125,13 @@ class FacilityTurretControl(turret: FacilityTurret) case _ => ; } + override protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + player: Player): Boolean = { + (!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed + } + override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = { tryAutoRepair() super.DamageAwareness(target, cause, amount) @@ -172,9 +178,9 @@ class FacilityTurretControl(turret: FacilityTurret) val zoneId = zone.id val events = zone.VehicleEvents turret.Seats.values.foreach(seat => - seat.Occupant match { + seat.occupant match { case Some(player) => - seat.Occupant = None + seat.unmount(player) player.VehicleSeated = None if (player.HasGUID) { events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala index 136c846c..def85e45 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala @@ -9,7 +9,9 @@ import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance * The definition for any `FacilityTurret`. * @param objectId the object's identifier number */ -class FacilityTurretDefinition(private val objectId: Int) extends AmenityDefinition(objectId) with TurretDefinition { +class FacilityTurretDefinition(private val objectId: Int) + extends AmenityDefinition(objectId) + with TurretDefinition { DamageUsing = DamageCalculations.AgainstVehicle ResistUsing = StandardVehicleResistance Model = SimpleResolutions.calculate diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala index 9c6e0de9..9936abab 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala @@ -2,7 +2,7 @@ package net.psforever.objects.serverobject.turret import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition} -import net.psforever.objects.vehicles.Turrets +import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets} import net.psforever.objects.vital.resistance.ResistanceProfileMutators import net.psforever.objects.vital.resolution.DamageResistanceModel @@ -11,14 +11,14 @@ import scala.collection.mutable /** * The definition for any `MannedTurret`. */ -trait TurretDefinition extends ResistanceProfileMutators with DamageResistanceModel { +trait TurretDefinition + extends MountableWeaponsDefinition + with ResistanceProfileMutators + with DamageResistanceModel { odef: ObjectDefinition => Turrets(odef.ObjectId) //let throw NoSuchElementException - /* key - entry point index, value - seat index */ - private val mountPoints: mutable.HashMap[Int, Int] = mutable.HashMap() - /* key - seat number, value - hash map (below) */ /* key - upgrade, value - weapon definition */ - private val weapons: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = + private val weaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]]() /** can only be mounted by owning faction when `true` */ @@ -29,9 +29,7 @@ trait TurretDefinition extends ResistanceProfileMutators with DamageResistanceMo */ private var hasReserveAmmunition: Boolean = false - def MountPoints: mutable.HashMap[Int, Int] = mountPoints - - def Weapons: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weapons + def WeaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weaponPaths def FactionLocked: Boolean = factionLocked diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala index 74b8d8b9..f9a036c4 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala @@ -1,22 +1,22 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.turret -import net.psforever.objects.{AmmoBox, PlanetSideGameObject, Player, Tool} -import net.psforever.objects.definition.{AmmoBoxDefinition, SeatDefinition, ToolDefinition} -import net.psforever.objects.equipment.{Equipment, EquipmentSlot} +import net.psforever.objects.{AmmoBox, PlanetSideGameObject, Tool} +import net.psforever.objects.definition.{AmmoBoxDefinition, ToolDefinition} +import net.psforever.objects.equipment.EquipmentSlot import net.psforever.objects.inventory.{Container, GridInventory} import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.vehicles.{MountedWeapons, Seat => Chair} +import net.psforever.objects.serverobject.mount.{SeatDefinition, Seat => Chair} +import net.psforever.objects.vehicles.MountableWeapons -trait WeaponTurret extends FactionAffinity with Mountable with MountedWeapons with Container { +trait WeaponTurret + extends FactionAffinity + with MountableWeapons + with Container { _: PlanetSideGameObject => - /** manned turrets have just one seat; this is just standard interface */ - protected val seats: Map[Int, Chair] = Map(0 -> Chair(new SeatDefinition() { ControlledWeapon = Some(1) })) - - /** turrets have just one weapon; this is just standard interface */ - protected var weapons: Map[Int, EquipmentSlot] = Map.empty + /** manned turrets have just one mount; this is just standard interface */ + seats = Map(0 -> new Chair(new SeatDefinition())) /** may or may not have inaccessible inventory space * see `ReserveAmmunition` in the definition @@ -45,39 +45,6 @@ trait WeaponTurret extends FactionAffinity with Mountable with MountedWeapons wi def VisibleSlots: Set[Int] = Set(1) - def Weapons: Map[Int, EquipmentSlot] = weapons - - def MountPoints: Map[Int, Int] - - def Seats: Map[Int, Chair] = seats - - def Seat(seatNum: Int): Option[Chair] = seats.get(seatNum) - - /** - * Given the index of an entry mounting point, return the infantry-accessible `Seat` associated with it. - * @param mountPoint an index representing the seat position / mounting point - * @return a seat number, or `None` - */ - def GetSeatFromMountPoint(mountPoint: Int): Option[Int] = { - MountPoints.get(mountPoint) - } - - def PassengerInSeat(user: Player): Option[Int] = { - if (seats(0).Occupant.contains(user)) { - Some(0) - } else { - None - } - } - - def ControlledWeapon(wepNumber: Int): Option[Equipment] = { - if (VisibleSlots.contains(wepNumber)) { - weapons(wepNumber).Equipment - } else { - None - } - } - def Upgrade: TurretUpgrade.Value = upgradePath def Upgrade_=(upgrade: TurretUpgrade.Value): TurretUpgrade.Value = { @@ -86,7 +53,7 @@ trait WeaponTurret extends FactionAffinity with Mountable with MountedWeapons wi //upgrade each weapon as long as that weapon has a valid option for that upgrade Definition match { case definition: TurretDefinition => - definition.Weapons.foreach({ + definition.WeaponPaths.foreach({ case (index, upgradePaths) => if (upgradePaths.contains(upgrade)) { updated = true @@ -136,7 +103,7 @@ object WeaponTurret { def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = { import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon //create weapons; note the class - turret.weapons = tdef.Weapons + turret.weapons = tdef.WeaponPaths .map({ case (num, upgradePaths) => val slot = EquipmentSlot(BaseTurretWeapon) @@ -146,7 +113,7 @@ object WeaponTurret { .toMap //special inventory ammunition object(s) if (tdef.ReserveAmmunition) { - val allAmmunitionTypes = tdef.Weapons.values.flatMap { _.values.flatMap { _.AmmoTypes } }.toSet + val allAmmunitionTypes = tdef.WeaponPaths.values.flatMap { _.values.flatMap { _.AmmoTypes } }.toSet if (allAmmunitionTypes.nonEmpty) { turret.inventory.Resize(allAmmunitionTypes.size, 1) var i: Int = 0 diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala index a1f6017f..ef3e9d78 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala @@ -46,7 +46,7 @@ object WeaponTurrets { * @param upgrade the upgrade being applied to the turret (usually, it's weapon system) */ def FinishUpgradingMannedTurret(target: FacilityTurret, upgrade: TurretUpgrade.Value): Unit = { - log.info(s"Converting manned wall turret weapon to $upgrade") + log.info(s"Manned wall turret weapon being converted to $upgrade") val zone = target.Zone val events = zone.VehicleEvents events ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.ClearSpecific(List(target), zone)) diff --git a/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala b/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala index 8e608ffe..860959c6 100644 --- a/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala +++ b/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala @@ -3,9 +3,9 @@ package net.psforever.objects.vehicles /** * An `Enumeration` of various permission groups that control access to aspects of a vehicle.
- * - `Driver` is a seat that is always seat number 0.
- * - `Gunner` is a seat that is not the `Driver` and controls a mounted weapon.
- * - `Passenger` is a seat that is not the `Driver` and does not have control of a mounted weapon.
+ * - `Driver` is a mount that is always mount number 0.
+ * - `Gunner` is a mount that is not the `Driver` and controls a mounted weapon.
+ * - `Passenger` is a mount that is not the `Driver` and does not have control of a mounted weapon.
* - `Trunk` represnts access to the vehicle's internal storage space.
* Organized to replicate the `PlanetsideAttributeMessage` value used for that given access level. * In their respective `PlanetsideAttributeMessage` packet, the groups are indexed in the same order as 10 through 13. diff --git a/src/main/scala/net/psforever/objects/vehicles/Cargo.scala b/src/main/scala/net/psforever/objects/vehicles/Cargo.scala index c2e1a71f..736b771e 100644 --- a/src/main/scala/net/psforever/objects/vehicles/Cargo.scala +++ b/src/main/scala/net/psforever/objects/vehicles/Cargo.scala @@ -1,88 +1,11 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2021 PSForever package net.psforever.objects.vehicles import net.psforever.objects.Vehicle -import net.psforever.objects.definition.{CargoDefinition} +import net.psforever.objects.serverobject.mount.{MountableSpace, MountableSpaceDefinition} -/** - * Server-side support for a slot that vehicles can occupy - * @param cargoDef the Definition that constructs this item and maintains some of its unchanging fields - */ -class Cargo(private val cargoDef: CargoDefinition) { - private var occupant: Option[Vehicle] = None +class Cargo(private val cdef: MountableSpaceDefinition[Vehicle]) extends MountableSpace[Vehicle] { + override protected def testToMount(target: Vehicle): Boolean = target.MountedIn.isEmpty && super.testToMount(target) - /** - * Is the cargo hold occupied? - * @return The vehicle in the cargo hold, or `None` if it is left vacant - */ - def Occupant: Option[Vehicle] = { - this.occupant - } - - /** - * A vehicle is trying to board the cargo hold - * Cargo holds are exclusive positions that can only hold one vehicle at a time. - * @param vehicle the vehicle boarding the cargo hold, or `None` if the vehicle is leaving - * @return the vehicle sitting in this seat, or `None` if it is left vacant - */ - def Occupant_=(vehicle: Vehicle): Option[Vehicle] = Occupant_=(Some(vehicle)) - - def Occupant_=(vehicle: Option[Vehicle]): Option[Vehicle] = { - if (vehicle.isDefined) { - if (this.occupant.isEmpty) { - this.occupant = vehicle - } - } else { - this.occupant = None - } - this.occupant - } - - /** - * Is this cargo hold occupied? - * @return `true`, if it is occupied; `false`, otherwise - */ - def isOccupied: Boolean = { - this.occupant.isDefined - } - - def CargoRestriction: CargoVehicleRestriction.Value = { - cargoDef.CargoRestriction - } - - def Bailable: Boolean = { - cargoDef.Bailable - } - - /** - * Override the string representation to provide additional information. - * @return the string output - */ - override def toString: String = { - Cargo.toString(this) - } -} - -object Cargo { - - /** - * Overloaded constructor. - * @return a `Cargo` object - */ - def apply(cargoDef: CargoDefinition): Cargo = { - new Cargo(cargoDef) - } - - /** - * Provide a fixed string representation. - * @return the string output - */ - def toString(obj: Cargo): String = { - val cargoStr = if (obj.isOccupied) { - s", occupied by vehicle ${obj.Occupant.get.GUID}" - } else { - "" - } - s"cargo$cargoStr" - } + 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 1262b3c3..be16cb45 100644 --- a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala @@ -147,9 +147,9 @@ object CargoBehavior { ) if (distance <= 64) { //cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it - log.info(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance") + log.debug(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance") cargo.MountedIn = carrierGUID - hold.Occupant = cargo + hold.mount(cargo) cargo.Velocity = None zone.VehicleEvents ! VehicleServiceMessage( s"${cargo.Actor}", @@ -160,15 +160,13 @@ object CargoBehavior { VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) ) val (attachMsg, mountPointMsg) = CargoMountBehaviorForAll(carrier, cargo, mountPoint) - log.info(s"HandleCheckCargoMounting: $attachMsg") - log.info(s"HandleCheckCargoMounting: $mountPointMsg") false } else if (distance > 625 || iteration >= 40) { //vehicles moved too far away or took too long to get into proper position; abort mounting - log.info( + 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 + val cargoDriverGUID = cargo.Seats(0).occupant.get.GUID zone.VehicleEvents ! VehicleServiceMessage( zone.id, VehicleAction.SendResponse( @@ -185,7 +183,7 @@ object CargoBehavior { ) ) false - //sending packet to the cargo vehicle's client results in player locking himself in his vehicle + //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 { @@ -263,10 +261,10 @@ object CargoBehavior { ) if (distance > 225) { //cargo vehicle has moved far enough away; close the carrier's hold door - log.info( + log.debug( s"HandleCheckCargoDismounting: dismount of cargo vehicle from carrier complete at distance of $distance" ) - val cargoDriverGUID = cargo.Seats(0).Occupant.get.GUID + val cargoDriverGUID = cargo.Seats(0).occupant.get.GUID zone.VehicleEvents ! VehicleServiceMessage( zone.id, VehicleAction.SendResponse( @@ -283,13 +281,13 @@ object CargoBehavior { ) ) false - //sending packet to the cargo vehicle's client results in player locking himself in his vehicle + //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 cargo.MountedIn = carrierGUID - hold.Occupant = cargo + hold.mount(cargo) CargoMountBehaviorForAll(carrier, cargo, mountPoint) false } else { @@ -363,11 +361,11 @@ object CargoBehavior { kicked: Boolean ): Unit = { val zone = carrier.Zone - carrier.CargoHolds.find({ case (_, hold) => hold.Occupant.contains(cargo) }) match { + carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match { case Some((mountPoint, hold)) => cargo.MountedIn = None - hold.Occupant = None - val driverOpt = cargo.Seats(0).Occupant + 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) @@ -393,7 +391,7 @@ object CargoBehavior { s"$cargoActor", VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) ) - if (carrier.Flying) { + if (carrier.isFlying) { //the carrier vehicle is flying; eject the cargo vehicle val ejectCargoMsg = CargoMountPointStatusMessage(carrierGUID, GUID0, GUID0, cargoGUID, mountPoint, CargoStatus.InProgress, 0) @@ -403,8 +401,7 @@ object CargoBehavior { events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, ejectCargoMsg)) events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, detachCargoMsg)) events ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(GUID0, resetCargoMsg)) - log.debug(ejectCargoMsg.toString) - log.debug(detachCargoMsg.toString) + 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() diff --git a/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala b/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala index 19b2b150..483fc06a 100644 --- a/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala +++ b/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestriction.scala @@ -2,11 +2,11 @@ package net.psforever.objects.vehicles /** - * An `Enumeration` of exo-suit-based seat access restrictions.
+ * An `Enumeration` of exo-suit-based mount access restrictions.
*
- * The default value is `NoMax` as that is the most common seat. + * The default value is `NoMax` as that is the most common mount. * `NoReinforcedOrMax` is next most common. - * `MaxOnly` is a rare seat restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. + * `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 diff --git a/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala b/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala new file mode 100644 index 00000000..fdbba2b2 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.serverobject.mount.Mountable + +trait MountableWeapons + extends MountedWeapons + with Mountable { + this: PlanetSideGameObject => + + /** + * Given a valid mount number, retrieve an index where the weapon controlled from this mount is mounted. + * @param seatNumber the mount number + * @return a mounted weapon by index, or `None` if either the mount doesn't exist or there is no controlled weapon + */ + def WeaponControlledFromSeat(seatNumber: Int): Option[Equipment] = { + Definition + .asInstanceOf[MountableWeaponsDefinition] + .controlledWeapons.get(seatNumber) match { + case Some(wepNumber) if seats.get(seatNumber).nonEmpty => controlledWeapon(wepNumber) + case _ => None + } + } + + def controlledWeapon(wepNumber: Int): Option[Equipment] = ControlledWeapon(wepNumber) + + def ControlledWeapon(wepNumber: Int): Option[Equipment] = { + weapons.get(wepNumber) match { + case Some(slot) => slot.Equipment + case _ => None + } + } + + def Definition: MountableWeaponsDefinition +} + diff --git a/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala b/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala new file mode 100644 index 00000000..f897b913 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala @@ -0,0 +1,12 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.serverobject.mount.MountableDefinition + +import scala.collection.mutable + +trait MountableWeaponsDefinition + extends MountedWeaponsDefinition + with MountableDefinition { + val controlledWeapons: mutable.HashMap[Int, Int] = mutable.HashMap[Int, Int]() +} diff --git a/src/main/scala/net/psforever/objects/vehicles/MountedWeapons.scala b/src/main/scala/net/psforever/objects/vehicles/MountedWeapons.scala index 35f67546..997e9f15 100644 --- a/src/main/scala/net/psforever/objects/vehicles/MountedWeapons.scala +++ b/src/main/scala/net/psforever/objects/vehicles/MountedWeapons.scala @@ -2,38 +2,13 @@ package net.psforever.objects.vehicles import net.psforever.objects.PlanetSideGameObject -import net.psforever.objects.equipment.{Equipment, EquipmentSlot} -import net.psforever.objects.inventory.Container -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.vehicles.{Seat => Chair} +import net.psforever.objects.equipment.EquipmentSlot trait MountedWeapons { - this: PlanetSideGameObject with Mountable with Container => + this: PlanetSideGameObject => + protected var weapons: Map[Int, EquipmentSlot] = Map[Int, EquipmentSlot]() - def Weapons: Map[Int, EquipmentSlot] + def Weapons: Map[Int, EquipmentSlot] = weapons - /** - * Given a valid seat number, retrieve an index where the weapon controlled from this seat is mounted. - * @param seatNumber the seat number - * @return a mounted weapon by index, or `None` if either the seat doesn't exist or there is no controlled weapon - */ - def WeaponControlledFromSeat(seatNumber: Int): Option[Equipment] = { - Seat(seatNumber) match { - case Some(seat) => - wepFromSeat(seat) - case None => - None - } - } - - private def wepFromSeat(seat: Chair): Option[Equipment] = { - seat.ControlledWeapon match { - case Some(index) => - ControlledWeapon(index) - case None => - None - } - } - - def ControlledWeapon(wepNumber: Int): Option[Equipment] + def Definition: MountedWeaponsDefinition } diff --git a/src/main/scala/net/psforever/objects/vehicles/MountedWeaponsDefinition.scala b/src/main/scala/net/psforever/objects/vehicles/MountedWeaponsDefinition.scala new file mode 100644 index 00000000..2ff759c7 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/MountedWeaponsDefinition.scala @@ -0,0 +1,13 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.definition.ToolDefinition + +import scala.collection.mutable + +trait MountedWeaponsDefinition { + /* key - mount index (where this weapon attaches during object construction), value - the weapon on an EquipmentSlot */ + protected var weapons: mutable.HashMap[Int, ToolDefinition] = mutable.HashMap[Int, ToolDefinition]() + + def Weapons: mutable.HashMap[Int, ToolDefinition] = weapons +} diff --git a/src/main/scala/net/psforever/objects/vehicles/Seat.scala b/src/main/scala/net/psforever/objects/vehicles/Seat.scala deleted file mode 100644 index 7da46728..00000000 --- a/src/main/scala/net/psforever/objects/vehicles/Seat.scala +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects.vehicles - -import net.psforever.objects.definition.SeatDefinition -import net.psforever.objects.Player - -/** - * Server-side support for a slot that infantry players can occupy, ostensibly called a "seat" and treated like a "seat." - * (Players can sit in it.) - * @param seatDef the Definition that constructs this item and maintains some of its unchanging fields - */ -class Seat(private val seatDef: SeatDefinition) { - private var occupant: Option[Player] = None -// private var lockState : VehicleLockState.Value = VehicleLockState.Empire - - /** - * Is this seat occupied? - * @return the Player object of the player sitting in this seat, or `None` if it is left vacant - */ - def Occupant: Option[Player] = { - this.occupant - } - - /** - * The player is trying to sit down. - * Seats are exclusive positions that can only hold one occupant at a time. - * @param player the player who wants to sit, or `None` if the occupant is getting up - * @return the Player object of the player sitting in this seat, or `None` if it is left vacant - */ - def Occupant_=(player: Player): Option[Player] = Occupant_=(Some(player)) - - def Occupant_=(player: Option[Player]): Option[Player] = { - if (player.isDefined) { - if (this.occupant.isEmpty) { - this.occupant = player - } - } else { - this.occupant = None - } - this.occupant - } - - /** - * Is this seat occupied? - * @return `true`, if it is occupied; `false`, otherwise - */ - def isOccupied: Boolean = { - this.occupant.isDefined - } - -// def SeatLockState : VehicleLockState.Value = { -// this.lockState -// } -// -// def SeatLockState_=(lockState : VehicleLockState.Value) : VehicleLockState.Value = { -// this.lockState = lockState -// SeatLockState -// } - - def ArmorRestriction: SeatArmorRestriction.Value = { - seatDef.ArmorRestriction - } - - def Bailable: Boolean = { - seatDef.Bailable - } - - def ControlledWeapon: Option[Int] = { - seatDef.ControlledWeapon - } - - /** - * Override the string representation to provide additional information. - * @return the string output - */ - override def toString: String = { - Seat.toString(this) - } -} - -object Seat { - - /** - * Overloaded constructor. - * @return a `Seat` object - */ - def apply(seatDef: SeatDefinition): Seat = { - new Seat(seatDef) - } - - /** - * Provide a fixed string representation. - * @return the string output - */ - def toString(obj: Seat): String = { - val seatStr = if (obj.isOccupied) { - s", occupied by player ${obj.Occupant.get.GUID}" - } else { - "" - } - s"seat$seatStr" - } -} diff --git a/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala b/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala index b811d76b..0da4c97c 100644 --- a/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala +++ b/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala @@ -2,14 +2,15 @@ package net.psforever.objects.vehicles /** - * An `Enumeration` of exo-suit-based seat access restrictions.
+ * An `Enumeration` of exo-suit-based mount access restrictions.
*
- * The default value is `NoMax` as that is the most common seat type. + * The default value is `NoMax` as that is the most common mount type. * `NoReinforcedOrMax` is next most common. - * `MaxOnly` is a rare seat restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. + * `MaxOnly` is a rare mount restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. + * `Unrestricted` is for "seats" that do not limit by exo-suit type, such the orbital shuttle. */ object SeatArmorRestriction extends Enumeration { type Type = Value - val MaxOnly, NoMax, NoReinforcedOrMax = Value + val MaxOnly, NoMax, NoReinforcedOrMax, Unrestricted = Value } diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index f197e53f..93492d76 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -5,6 +5,7 @@ import akka.actor.{Actor, Cancellable} import net.psforever.objects._ import net.psforever.objects.ballistics.VehicleSource import net.psforever.objects.ce.TelepadLike +import net.psforever.objects.entity.WorldEntity import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons} import net.psforever.objects.guid.GUIDTask import net.psforever.objects.inventory.{GridInventory, InventoryItem} @@ -48,8 +49,7 @@ class VehicleControl(vehicle: Vehicle) extends Actor with FactionAffinityBehavior.Check with DeploymentBehavior - with MountableBehavior.Mount - with MountableBehavior.Dismount + with MountableBehavior with CargoBehavior with DamageableVehicle with RepairableVehicle @@ -129,34 +129,13 @@ class VehicleControl(vehicle: Vehicle) case Vehicle.Ownership(Some(player)) => GainOwnership(player) - case msg@Mountable.TryMount(player, seat_num) => - tryMountBehavior.apply(msg) - val obj = MountableObject - //check that the player has actually been sat in the expected seat - if (obj.PassengerInSeat(player).contains(seat_num)) { - //if the driver seat, change ownership - if (seat_num == 0 && !obj.OwnerName.contains(player.Name)) { - //whatever vehicle was previously owned - vehicle.Zone.GUID(player.avatar.vehicle) match { - case Some(v : Vehicle) => - v.Actor ! Vehicle.Ownership(None) - case _ => - player.avatar.vehicle = None - } - LoseOwnership() //lose our current ownership - GainOwnership(player) //gain new ownership - } - else { - decaying = false - decayTimer.cancel() - } - // - updateZoneInteractionProgressUI(player) - } + case msg @ Mountable.TryMount(player, mount_point) => + mountBehavior.apply(msg) + mountCleanup(mount_point, player) - case msg : Mountable.TryDismount => + case msg @ Mountable.TryDismount(_, seat_num) => dismountBehavior.apply(msg) - dismountCleanup() + dismountCleanup(seat_num) case Vehicle.ChargeShields(amount) => val now : Long = System.currentTimeMillis() @@ -261,7 +240,7 @@ class VehicleControl(vehicle: Vehicle) case Vehicle.Deconstruct(time) => time match { - case Some(delay) => + case Some(delay) if vehicle.Definition.undergoesDecay => decaying = true decayTimer.cancel() decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) @@ -276,6 +255,9 @@ class VehicleControl(vehicle: Vehicle) PrepareForDeletion() context.become(ReadyToDelete) + case VehicleControl.AssignOwnership(player) => + vehicle.AssignOwnership(player) + case _ => ; } @@ -285,13 +267,13 @@ class VehicleControl(vehicle: Vehicle) case msg : Deployment.TryUndeploy => deployBehavior.apply(msg) - case msg : Mountable.TryDismount => + case msg @ Mountable.TryDismount(_, seat_num) => dismountBehavior.apply(msg) - dismountCleanup() + dismountCleanup(seat_num) case Vehicle.Deconstruct(time) => time match { - case Some(delay) => + case Some(delay) if vehicle.Definition.undergoesDecay => decaying = true decayTimer.cancel() decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) @@ -324,46 +306,77 @@ class VehicleControl(vehicle: Vehicle) case _ => } - val tryMountBehavior : Receive = { - case msg @ Mountable.TryMount(user, seat_num) => - val exosuit = user.ExoSuit - val restriction = vehicle.Seats(seat_num).ArmorRestriction - val seatGroup = vehicle.SeatPermissionGroup(seat_num).getOrElse(AccessPermissionGroup.Passenger) - val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) - if ( - (if (seatGroup == AccessPermissionGroup.Driver) { - vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked - } - else { - permission != VehicleLockState.Locked - }) && - (exosuit match { - case ExoSuitType.MAX => restriction == SeatArmorRestriction.MaxOnly - case ExoSuitType.Reinforced => restriction == SeatArmorRestriction.NoMax - case _ => restriction != SeatArmorRestriction.MaxOnly - }) - ) { - mountBehavior.apply(msg) - } - else { - sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, seat_num)) - } + override protected def mountTest( + obj: PlanetSideServerObject with Mountable, + seatNumber: Int, + user: Player + ): Boolean = { + val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger) + val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) + (if (seatGroup == AccessPermissionGroup.Driver) { + vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked + } else { + permission != VehicleLockState.Locked + }) && + super.mountTest(obj, seatNumber, user) } - def dismountCleanup(): Unit = { + def mountCleanup(mount_point: Int, user: Player): Unit = { + val obj = MountableObject + obj.PassengerInSeat(user) match { + case Some(seatNumber) => + //if the driver mount, change ownership if that is permissible for this vehicle + if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) { + //whatever vehicle was previously owned + vehicle.Zone.GUID(user.avatar.vehicle) match { + case Some(v : Vehicle) => + v.Actor ! Vehicle.Ownership(None) + case _ => + user.avatar.vehicle = None + } + GainOwnership(user) //gain new ownership + } + else { + decaying = false + decayTimer.cancel() + } + updateZoneInteractionProgressUI(user) + case None => ; + } + } + + override protected def dismountTest( + obj: Mountable with WorldEntity, + seatNumber: Int, + user: Player + ): Boolean = { + vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user) + } + + def dismountCleanup(seatBeingDismounted: Int): Unit = { val obj = MountableObject // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount if (!obj.Seats(0).isOccupied) { obj.Velocity = Some(Vector3.Zero) } - //are we already decaying? are we unowned? is no one seated anywhere? - if (!decaying && obj.Owner.isEmpty && obj.Seats.values.forall(!_.isOccupied)) { - decaying = true - decayTimer = context.system.scheduler.scheduleOnce( - MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), - self, - VehicleControl.PrepareForDeletion() - ) + if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated + //we were only owning the vehicle while we sat in its driver seat + val canBeOwned = obj.Definition.CanBeOwned + if (canBeOwned.contains(false) && seatBeingDismounted == 0) { + LoseOwnership() + } + //are we already decaying? are we unowned? is no one seated anywhere? + if (!decaying && + obj.Definition.undergoesDecay && + obj.Owner.isEmpty && + obj.Seats.values.forall(!_.isOccupied)) { + decaying = true + decayTimer = context.system.scheduler.scheduleOnce( + MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), + self, + VehicleControl.PrepareForDeletion() + ) + } } } @@ -387,12 +400,12 @@ class VehicleControl(vehicle: Vehicle) ) case _ => ; } - if (!vehicle.Flying || kickPassengers) { + if (!vehicle.isFlying || kickPassengers) { //kick all passengers (either not flying, or being explicitly instructed) vehicle.Seats.values.foreach { seat => - seat.Occupant match { + seat.occupant match { case Some(player) => - seat.Occupant = None + seat.unmount(player) player.VehicleSeated = None if (player.HasGUID) { events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) @@ -405,7 +418,7 @@ class VehicleControl(vehicle: Vehicle) vehicle.CargoHolds.values .collect { case hold if hold.isOccupied => - val cargo = hold.Occupant.get + val cargo = hold.occupant.get CargoBehavior.HandleVehicleCargoDismount( cargo.GUID, cargo, @@ -420,10 +433,7 @@ class VehicleControl(vehicle: Vehicle) def PrepareForDeletion() : Unit = { decaying = false - val guid = vehicle.GUID val zone = vehicle.Zone - val zoneId = zone.id - val events = zone.VehicleEvents //miscellaneous changes Vehicles.BeforeUnloadVehicle(vehicle, zone) //cancel jammed behavior @@ -446,7 +456,10 @@ class VehicleControl(vehicle: Vehicle) def LoseOwnership(): Unit = { val obj = MountableObject Vehicles.Disown(obj.GUID, obj) - if (!decaying && obj.Seats.values.forall(!_.isOccupied)) { + if (!decaying && + obj.Definition.undergoesDecay && + obj.Owner.isEmpty && + obj.Seats.values.forall(!_.isOccupied)) { decaying = true decayTimer = context.system.scheduler.scheduleOnce( obj.Definition.DeconstructionTime.getOrElse(5 minutes), @@ -457,7 +470,9 @@ class VehicleControl(vehicle: Vehicle) } def GainOwnership(player: Player): Unit = { - Vehicles.Own(MountableObject, player) match { + val obj = MountableObject + Vehicles.Disown(obj.GUID, obj) + Vehicles.Own(obj, player) match { case Some(_) => decaying = false decayTimer.cancel() @@ -535,7 +550,7 @@ class VehicleControl(vehicle: Vehicle) val toChannel = if (obj.VisibleSlots.contains(fromSlot)) zone.id else self.toString zone.VehicleEvents ! VehicleServiceMessage( toChannel, - VehicleAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID) + VehicleAction.ObjectDelete(item.GUID) ) } @@ -555,7 +570,7 @@ class VehicleControl(vehicle: Vehicle) val zone = vehicle.Zone val zoneChannel = zone.id val GUID0 = Service.defaultPlayerGUID - val driverChannel = vehicle.Seats(0).Occupant match { + val driverChannel = vehicle.Seats(0).occupant match { case Some(tplayer) => tplayer.Name case None => "" } @@ -620,7 +635,7 @@ class VehicleControl(vehicle: Vehicle) val guid = vehicle.GUID val zone = vehicle.Zone val GUID0 = Service.defaultPlayerGUID - val driverChannel = vehicle.Seats(0).Occupant match { + val driverChannel = vehicle.Seats(0).occupant match { case Some(tplayer) => tplayer.Name case None => "" } @@ -683,7 +698,7 @@ class VehicleControl(vehicle: Vehicle) percentage, body, vehicle.Seats.values - .collect { case seat if seat.isOccupied => seat.Occupant.get } + .flatMap { case seat if seat.isOccupied => seat.occupants } .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) } ) } @@ -776,7 +791,7 @@ class VehicleControl(vehicle: Vehicle) percentage, body, vehicle.Seats.values - .collect { case seat if seat.isOccupied => seat.Occupant.get } + .flatMap { case seat if seat.isOccupied => seat.occupants } .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) } ) } @@ -860,6 +875,8 @@ object VehicleControl { private case class Deletion() + final case class AssignOwnership(player: Option[Player]) + /** * Determine if a given activity entry would invalidate the act of charging vehicle shields this tick. * @param now the current time (in nanoseconds) diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala index fcf1f544..2101ff34 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala @@ -2,6 +2,7 @@ package net.psforever.objects.vehicles import net.psforever.objects.Vehicle +import net.psforever.objects.serverobject.mount.Seat import net.psforever.objects.zones.Zone /** @@ -14,7 +15,7 @@ import net.psforever.objects.zones.Zone * @param vehicle the vehicle in transport * @param origin where the vehicle originally was * @param driverName the name of the driver when the transport process started - * @param passengers the paired names and seat indices of all passengers when the transport process started + * @param passengers the paired names and mount indices of all passengers when the transport process started * @param cargo the paired driver names and cargo hold indices of all cargo vehicles when the transport process started */ final case class VehicleManifest( @@ -28,17 +29,17 @@ final case class VehicleManifest( object VehicleManifest { def apply(vehicle: Vehicle): VehicleManifest = { - val driverName = vehicle.Seats(0).Occupant match { + val driverName = vehicle.Seats(0).occupant match { case Some(driver) => driver.Name case None => "MISSING_DRIVER" } val passengers = vehicle.Seats.collect { - case (index, seat) if index > 0 && seat.isOccupied => - (seat.Occupant.get.Name, index) + case (index: Int, seat: Seat) if index > 0 && seat.isOccupied => + (seat.occupant.get.Name, index) } val cargo = vehicle.CargoHolds.collect { - case (index, hold) if hold.Occupant.nonEmpty => - hold.Occupant.get.Seats(0).Occupant match { + case (index: Int, hold: Cargo) if hold.occupant.nonEmpty => + hold.occupant.get.Seats(0).occupant match { case Some(driver) => (driver.Name, index) case None => diff --git a/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala b/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala new file mode 100644 index 00000000..3693b9e1 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala @@ -0,0 +1,48 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vital.etc + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.ballistics.SourceEntry +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.{DamageReason, DamageResolution} +import net.psforever.objects.vital.prop.DamageWithPosition +import net.psforever.objects.vital.resolution.DamageAndResistance + +/** + * A wrapper for a "damage source" in damage calculations + * that parameterizes information necessary to explain a server-driven electromagnetic pulse occurring. + * @see `VitalityDefinition.explodes` + * @see `VitalityDefinition.innateDamage` + * @see `Zone.causesSpecialEmp` + * @param entity the source of the explosive yield + * @param damageModel the model to be utilized in these calculations; + * typically, but not always, defined by the target + */ +final case class EmpReason( + entity: SourceEntry, + source: DamageWithPosition, + damageModel: DamageAndResistance, + override val attribution: Int + ) extends DamageReason { + def resolution: DamageResolution.Value = DamageResolution.Splash + + def same(test: DamageReason): Boolean = test match { + case eer: ExplodingEntityReason => eer.entity eq entity + case _ => false + } + + /** lay the blame on that which caused this emp to occur */ + def adversary: Option[SourceEntry] = Some(entity) +} + +object EmpReason { + def apply( + owner: PlanetSideGameObject with FactionAffinity, + source: DamageWithPosition, + target: PlanetSideServerObject with Vitality + ): EmpReason = { + EmpReason(SourceEntry(owner), source, target.DamageModel, owner.Definition.ObjectId) + } +} diff --git a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala index 7e652ff1..ed1096ab 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala @@ -5,10 +5,11 @@ import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.ballistics.SourceEntry import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.vital.{Vitality, VitalityDefinition} -import net.psforever.objects.vital.base.{DamageReason, DamageResolution} -import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution} +import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital.resolution.DamageAndResistance +import net.psforever.objects.zones.Zone /** * A wrapper for a "damage source" in damage calculations @@ -48,3 +49,48 @@ final case class ExplodingEntityReason( /** the entity that exploded is the source of the damage */ override def attribution: Int = definition.ObjectId } + +object ExplodingDamageModifiers { + trait Mod extends DamageModifiers.Mod { + def calculate(damage : Int, data : DamageInteraction, cause : DamageReason) : Int = { + cause match { + case o: ExplodingEntityReason => calculate(damage, data, o) + case _ => damage + } + } + + def calculate(damage : Int, data : DamageInteraction, cause : ExplodingEntityReason) : Int + } +} + +/** + * A variation of the normal radial damage degradation + * that uses the geometric representations of the exploding entity and of the affected target + * in its calculations that determine the distance between them. + * @see `DamageModifierFunctions.RadialDegrade` + */ +case object ExplodingRadialDegrade extends ExplodingDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: ExplodingEntityReason): Int = { + cause.source match { + case withPosition: DamageWithPosition => + val radius = withPosition.DamageRadius + val radiusMin = withPosition.DamageRadiusMin + val distance = math.sqrt(Zone.distanceCheck( + cause.entity.Definition.asInstanceOf[ObjectDefinition].Geometry(cause.entity), + data.target.Definition.Geometry(data.target) + )) + if (distance <= radiusMin) { + damage + } else if (distance <= radius) { + //damage - (damage * profile.DamageAtEdge * (distance - radiusMin) / (radius - radiusMin)).toInt + val base = withPosition.DamageAtEdge + val radi = radius - radiusMin + (damage * ((1 - base) * ((radi - (distance - radiusMin)) / radi) + base)).toInt + } else { + 0 + } + case _ => + damage + } + } +} diff --git a/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala b/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala new file mode 100644 index 00000000..b25531a8 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala @@ -0,0 +1,62 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.vital.etc + +import net.psforever.objects.GlobalDefinitions +import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} +import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} +import net.psforever.objects.vital.base.{DamageReason, DamageResolution} +import net.psforever.objects.vital.damage.DamageCalculations.AgainstExoSuit +import net.psforever.objects.vital.prop.DamageProperties +import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} +import net.psforever.types.PlanetSideGUID + +/** + * A wrapper for a "damage source" in damage calculations + * that parameterizes information necessary to explain a `BoomerDeployable` being detonated + * using its complementary trigger. + * Should be applied as the reason applied to the Boomer + * in `DamageInteractions` that lead up to the Boomer exploding + * which will carry the trigger as the reason and the user as the culprit. + * Due to faction affiliation complicity between the user and the Boomer, however, + * normal `Damageable` functionality would have to interject in a way where the trigger works anyway. + * @see `BoomerDeployable` + * @see `BoomerTrigger` + * @see `DamageCalculations` + * @see `VitalityDefinition.DamageableByFriendlyFire` + * @param user the player who is holding the trigger + * @param item_guid the trigger + */ +final case class TriggerUsedReason(user: PlayerSource, item_guid: PlanetSideGUID) + extends DamageReason { + def source: DamageProperties = TriggerUsedReason.triggered + + def resolution: DamageResolution.Value = DamageResolution.Resolved + + def same(test: DamageReason): Boolean = test match { + case tur: TriggerUsedReason => tur.item_guid == item_guid && tur.user.Name.equals(user.Name) + case _ => false + } + + /** lay the blame on the player who caused this explosion to occur */ + def adversary: Option[SourceEntry] = Some(user) + + override def damageModel : DamageAndResistance = TriggerUsedReason.drm + + /** while weird, the trigger was accredited as the method of death on Gemini Live; + * even though its icon looks like an misshapen AMS */ + override def attribution: Int = GlobalDefinitions.boomer_trigger.ObjectId +} + +object TriggerUsedReason { + private val triggered = new DamageProperties { + Damage0 = 1 //token damage + SympatheticExplosion = true //sets off a boomer + } + + /** basic damage, no resisting, quick and simple */ + private val drm = new DamageResistanceModel { + DamageUsing = AgainstExoSuit + ResistUsing = NoResistanceSelection + Model = SimpleResolutions.calculate + } +} \ No newline at end of file diff --git a/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala b/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala index da2178fb..7157375a 100644 --- a/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala +++ b/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala @@ -392,7 +392,7 @@ object ProjectileDamageModifierFunctions { data: DamageInteraction, cause: ProjectileReason ): Int = { - if (cause.resolution == resolution) { + if (data.resolution == resolution) { (data.cause.source.Aggravated, data.target) match { case (Some(aggravation), p: PlayerSource) => val degradation = aggravation.info.find(_.damage_type == damageType) match { diff --git a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala index dc3daf17..c8e83fed 100644 --- a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -6,9 +6,11 @@ import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} import net.psforever.objects.ce.ComplexDeployable import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.damage.Damageable -import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.DamageResolution +import net.psforever.objects.vital.{DamagingActivity, Vitality, VitalsHistory} import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} +import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.resistance.ResistanceSelection import net.psforever.types.{ExoSuitType, ImplantType} @@ -153,7 +155,7 @@ object ResolutionCalculations { def InfantryApplication(damageValues: (Int, Int), data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = { val targetBefore = SourceEntry(target) target match { - case player: Player => + case player: Player if noDoubleLash(player, data) => var (a, b) = damageValues if (player.isAlive && !(a == 0 && b == 0)) { val originalHealth = player.Health @@ -323,4 +325,17 @@ object ResolutionCalculations { SimpleApplication(dam, data)(target) } } + + private def noDoubleLash(target: PlanetSideGameObject with VitalsHistory, data: DamageInteraction): Boolean = { + data.cause match { + case reason: ProjectileReason if reason.resolution == DamageResolution.Lash => + val curr = System.currentTimeMillis() + !target.History.exists { + case dam: DamagingActivity => curr - dam.time < 1000 && reason.same(dam.data.interaction.cause) + case _ => false + } + case _ => + true + } + } } diff --git a/src/main/scala/net/psforever/objects/zones/MapInfo.scala b/src/main/scala/net/psforever/objects/zones/MapInfo.scala index 8191c47b..d96582e9 100644 --- a/src/main/scala/net/psforever/objects/zones/MapInfo.scala +++ b/src/main/scala/net/psforever/objects/zones/MapInfo.scala @@ -2,7 +2,7 @@ package net.psforever.objects.zones import enumeratum.values.{StringEnum, StringEnumEntry} import net.psforever.objects.serverobject.environment._ -import net.psforever.types.Vector3 +import net.psforever.types.{PlanetSideGUID, Vector3} sealed abstract class MapInfo( val value: String, @@ -64,7 +64,7 @@ case object MapInfo extends StringEnum[MapInfo] { checksum = 1624200906L, scale = MapScale.Dim8192, environment = List( - SeaLevel(EnvironmentAttribute.Water, 35), + SeaLevel(EnvironmentAttribute.Water, 29.5f), Pool(EnvironmentAttribute.Water, 67.3125f, 3449.586f, 5870.383f, 3313.75f, 5715.3203f), //east of itan, south of kaang Pool(EnvironmentAttribute.Water, 53.71875f, 6013.0625f, 1861.7969f, 5947.1406f, 1634.7734f), //E6 Pool(EnvironmentAttribute.Water, 49.625f, 7181.6953f, 1496.3828f, 6972.992f, 1340.1328f), //east of wele @@ -180,7 +180,7 @@ case object MapInfo extends StringEnum[MapInfo] { Pool(EnvironmentAttribute.Water, 34.96875f, 5899.367f, 3235.5781f, 5573.8516f, 2865.7812f), //northeast of hart c campus Pool(EnvironmentAttribute.Water, 34.328125f, 3880.7422f, 5261.508f, 3780.9219f, 5166.953f), //east of hart a campus Pool(EnvironmentAttribute.Water, 31.03125f, 4849.797f, 2415.4297f, 4731.8594f, 2252.1484f) //south of hart c campus - ) + ) ++ MapEnvironment.map11Environment ) case object Map12 @@ -188,7 +188,8 @@ case object MapInfo extends StringEnum[MapInfo] { value = "map12", checksum = 962888126L, scale = MapScale.Dim8192, - environment = List(SeaLevel(EnvironmentAttribute.Water, 20.03125f)) + environment = List(SeaLevel(EnvironmentAttribute.Water, 20.03125f)) ++ + MapEnvironment.map12Environment ) case object Map13 @@ -196,7 +197,8 @@ case object MapInfo extends StringEnum[MapInfo] { value = "map13", checksum = 3904659548L, scale = MapScale.Dim8192, - environment = List(SeaLevel(EnvironmentAttribute.Water, 30)) + environment = List(SeaLevel(EnvironmentAttribute.Water, 30)) ++ + MapEnvironment.map13Environment ) case object Map14 @@ -317,3 +319,107 @@ case object MapInfo extends StringEnum[MapInfo] { val values: IndexedSeq[MapInfo] = findValues } + +object MapEnvironment { + /** the pattern of mount points for the HART gantries in most facilities; + * eight values - 1-8 - listed as four downstairs - NE SE NW SW - then four upstairs - same + */ + private val hartMountPoints: Seq[Int] = Seq(6,5, 2,1, 8,7, 4,3) + /** the pattern of mount points for the HART gantries in VS sanctuary facilities; + * eight values - 1-8 - listed as four downstairs - NE SE NW SW - then four upstairs - same + */ + private val vsHartMountPoints: Seq[Int] = Seq(1,2, 5,6, 3,4, 7,8) + + /** HART denial fields for the New Conglomerate sanctuary */ + final val map11Environment: List[PieceOfEnvironment] = + hartGantryDenialFields(PlanetSideGUID(840), Vector3(2258, 5538, 65.20142f), hartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(841), Vector3(4152, 6070, 43.8766136f), hartMountPoints) ++ + specialHartGantryDenialFields(PlanetSideGUID(842)) + + /** HART denial fields for the Terran Republic sanctuary */ + final val map12Environment: List[PieceOfEnvironment] = + hartGantryDenialFields(PlanetSideGUID(808), Vector3(2922, 5230, 35.9989929f), hartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(809), Vector3(3006, 2984, 34.919342f), hartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(810), Vector3(5232, 3908, 35.9291039f), hartMountPoints) + + /** HART denial fields for the Vanu Sovereignty sanctuary */ + final val map13Environment: List[PieceOfEnvironment] = + hartGantryDenialFields(PlanetSideGUID(786), Vector3(2978, 4834, 56.085392f), vsHartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(787), Vector3(3688, 2808, 90.85312f), vsHartMountPoints) ++ + hartGantryDenialFields(PlanetSideGUID(788), Vector3(5610, 4238, 103.228859f), vsHartMountPoints) + + /** + * Generate eight environmental representations that serve to eject players + * from the high altitude rapid transport (HART) building boarding gantry hallways + * when the HART shuttle associated with that building is no longer boarding + * and the doors to those hallways should deny entrance. + * When kicked out of the hallway, + * ejected players should be placed in the same position as if the player willingly dismounted the shuttle.
+ *
+ * While this task seems daunting, HART buildings are formulaic, not only in layout but in orientation. + * @param obbasemesh the globally unique identifier of the orbital shuttle pad, + * an amenity of an `orbital_building_*` + * @param position a very specific position near the center of the `orbital_building_*` building + * @param mountPoints the assignment of mount point for each denial field + * @return a list of environmental representations + */ + private def hartGantryDenialFields( + obbasemesh: PlanetSideGUID, + position: Vector3, + mountPoints: Seq[Int] + ): List[PieceOfEnvironment] = { + val px: Float = position.x + val py: Float = position.y + val pz: Float = position.z + val wall: Float = 14.7188f + val door: Float = 55.9219f + val gantry: Float = 45.9297f + val lower: Float = pz + 6.164608f + val upper: Float = pz + 17.508358f + //downstairs lobbies are listed before upstairs lobbies to ensure they are tested first + List( + GantryDenialField(obbasemesh, mountPoints(0), DeepSurface(lower, py + wall, px + door, py + 1, px + gantry)), //NE + GantryDenialField(obbasemesh, mountPoints(1), DeepSurface(lower, py - 1, px + door, py - wall, px + gantry)), //SE + GantryDenialField(obbasemesh, mountPoints(2), DeepSurface(lower, py + wall, px - gantry, py + 1, px - door)), //NW + GantryDenialField(obbasemesh, mountPoints(3), DeepSurface(lower, py - 1, px - gantry, py - wall, px - door)), //SW + GantryDenialField(obbasemesh, mountPoints(4), DeepSurface(upper, py + wall, px + door, py + 1, px + gantry)), //NE + GantryDenialField(obbasemesh, mountPoints(5), DeepSurface(upper, py - 1, px + door, py - wall, px + gantry)), //SE + GantryDenialField(obbasemesh, mountPoints(6), DeepSurface(upper, py + wall, px - gantry, py + 1, px - door)), //NW + GantryDenialField(obbasemesh, mountPoints(7), DeepSurface(upper, py - 1, px - gantry, py - wall, px - door)) //SW + ) + } + + /** + * Generate eight environmental representations that serve to eject players + * from the high altitude rapid transport (HART) building boarding hallways + * when the HART shuttle associated with that building is no longer boarding + * and the doors to those hallways should deny entrance. + * When kicked out of the hallway, + * ejected players should be placed in the same position as if the player willingly dismounted the shuttle.
+ *
+ * The New Conglomerate HART A campus building is at an ordinal angle + * which makes the typical axis-aligned environment geometry unsuitable for representation of the denial field. + * Instead of rectangles, circles will be used. + * This facility is centered at 4816, 3506, 68.73806 (x ,y, z). + * @param obbasemesh the globally unique identifier of the orbital shuttle pad, + * an amenity of an `orbital_building_*` + * @return a list of environmental representations + */ + def specialHartGantryDenialFields(obbasemesh: PlanetSideGUID): List[PieceOfEnvironment] = { + val lower: Float = 74.902668f + val upper: Float = 86.246418f + val radius: Float = 6.5f + //downstairs lobbies are listed before upstairs lobbies to ensure they are tested first + List( + GantryDenialField(obbasemesh, 1, DeepCircularSurface(Vector3(4846f, 3547.6016f, lower), radius)), //N + GantryDenialField(obbasemesh, 2, DeepCircularSurface(Vector3(4857.5234f, 3536f, lower), radius)), //E + GantryDenialField(obbasemesh, 5, DeepCircularSurface(Vector3(4774.3516f, 3476f, lower), radius)), //W + GantryDenialField(obbasemesh, 6, DeepCircularSurface(Vector3(4786f, 3464.4453f, lower), radius)), //S + GantryDenialField(obbasemesh, 3, DeepCircularSurface(Vector3(4846f, 3547.6016f, upper), radius)), //N + GantryDenialField(obbasemesh, 4, DeepCircularSurface(Vector3(4857.5234f, 3536f, upper), radius)), //E + GantryDenialField(obbasemesh, 7, DeepCircularSurface(Vector3(4774.3516f, 3476f, upper), radius)), //W + GantryDenialField(obbasemesh, 8, DeepCircularSurface(Vector3(4786f, 3464.4453f, upper), radius)) //S + ) + } +} + diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 17f393bc..92d17f65 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -38,13 +38,19 @@ import akka.actor.typed import net.psforever.actors.session.AvatarActor import net.psforever.actors.zone.ZoneActor import net.psforever.objects.avatar.Avatar +import net.psforever.objects.geometry.Geometry3D import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles.UtilityType -import net.psforever.objects.vital.etc.ExplodingEntityReason -import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.etc.{EmpReason, ExplodingEntityReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} +import net.psforever.objects.vital.prop.DamageWithPosition +import net.psforever.objects.vital.Vitality +import net.psforever.services.Service /** * A server object representing the one-landmass planets as well as the individual subterranean caverns.
@@ -645,6 +651,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { case (None, _) | (_, None) => ; //let ZoneActor's sanity check catch this error } }) + //doors with nearby locks use those locks as their unlocking mechanism + //let ZoneActor's sanity check catch missing entities + map.doorToLock + .map { case(doorGUID: Int, lockGUID: Int) => (guid(doorGUID), guid(lockGUID)) } + .collect { case (Some(door: Door), Some(lock: IFFLock)) => + door.Actor ! Door.UpdateMechanism(IFFLock.testLock(lock)) + } //ntu management (eventually move to a generic building startup function) buildings.values .flatMap(_.Amenities.filter(_.Definition == GlobalDefinitions.resource_silo)) @@ -659,6 +672,12 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { case painbox: Painbox => painbox.Actor ! "startup" } + //the orbital_buildings in sanctuary zones have to establish their shuttle routes + map.shuttleBays + .map { guid(_) } + .collect { case Some(obj: OrbitalShuttlePad) => + obj.Actor ! Service.Startup() + } //allocate soi information soi ! SOI.Build() } @@ -922,6 +941,10 @@ object Zone { final case class Despawn(vehicle: Vehicle) + final case class HasSpawned(zone: Zone, vehicle: Vehicle) + + final case class HasDespawned(zone: Zone, vehicle: Vehicle) + final case class CanNotSpawn(zone: Zone, vehicle: Vehicle, reason: String) final case class CanNotDespawn(zone: Zone, vehicle: Vehicle, reason: String) @@ -1135,7 +1158,7 @@ object Zone { .flatMap { _.Amenities } .filter { _.Definition.Damageable } } - //restrict to targets in the damage radius + //restrict to targets according to the detection plan val allAffectedTargets = (playerTargets ++ vehicleTargets ++ complexDeployableTargets ++ soiTargets) .filter { target => (target ne obj) && detectionTest(obj, target, radius) @@ -1172,19 +1195,107 @@ object Zone { } } + /** + * Allocates `Damageable` targets within the radius of a server-prepared electromagnetic pulse + * and informs those entities that they have affected by the aforementioned pulse. + * Targets within the effect radius within other rooms are affected, unlike with normal damage. + * The only affected target is Boomer deployables. + * @see `Amenity.Owner` + * @see `BoomerDeployable` + * @see `DamageInteraction` + * @see `DamageResult` + * @see `DamageWithPosition` + * @see `EmpReason` + * @see `Zone.DeployableList` + * @param zone the zone in which the emp should occur + * @param obj the entity that triggered the emp (information) + * @param sourcePosition where the emp physically originates + * @param effect characteristics of the emp produced + * @param detectionTest a custom test to determine if any given target is affected; + * defaults to an internal test for simple radial proximity + * @return a list of affected entities + */ + def causeSpecialEmp( + zone: Zone, + obj: PlanetSideServerObject with Vitality, + sourcePosition: Vector3, + effect: DamageWithPosition, + detectionTest: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck + ): List[PlanetSideServerObject] = { + val proxy: ExplosiveDeployable = { + //construct a proxy unit to represent the pulse + val o = new ExplosiveDeployable(GlobalDefinitions.special_emp) + o.Owner = Some(obj.GUID) + o.OwnerName = obj match { + case p: Player => p.Name + case o: OwnableByPlayer => o.OwnerName.getOrElse("") + case _ => "" + } + o.Position = sourcePosition + o.Faction = obj.Faction + o + } + val radius = effect.DamageRadius * effect.DamageRadius + //only boomers can be affected (that's why it's special) + val allAffectedTargets = zone.DeployableList + .collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) && detectionTest(proxy, o, radius) => o } + //inform targets that they have suffered the effects of the emp + allAffectedTargets + .foreach { target => + target.Actor ! Vitality.Damage( + DamageInteraction( + SourceEntry(target), + EmpReason(obj, effect, target), + sourcePosition + ).calculate() + ) + } + allAffectedTargets + } + /** * Two game entities are considered "near" each other if they are within a certain distance of one another. * A default function literal mainly used for `causesExplosion`. * @see `causeExplosion` - * @see `Vector3.DistanceSquare` - * @param obj1 a game entity - * @param obj2 a game entity + * @see `ObjectDefinition.Geometry` + * @param obj1 a game entity, should be the source of the explosion + * @param obj2 a game entity, should be the target of the explosion * @param maxDistance the square of the maximum distance permissible between game entities * before they are no longer considered "near" - * @return `true`, if the target entities are near to each other; + * @return `true`, if the target entities are near enough to each other; * `false`, otherwise */ - private def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = { - Vector3.DistanceSquared(obj1.Position, obj2.Position) <= maxDistance + def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = { + distanceCheck(obj1.Definition.Geometry(obj1), obj2.Definition.Geometry(obj2), maxDistance) + } + + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * @param g1 the geometric representation of a game entity + * @param g2 the geometric representation of a game entity + * @param maxDistance the square of the maximum distance permissible between game entities + * before they are no longer considered "near" + * @return `true`, if the target entities are near enough to each other; + * `false`, otherwise + */ + def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = { + Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3) <= maxDistance || + distanceCheck(g1, g2) <= maxDistance + } + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * @see `PrimitiveGeometry.pointOnOutside` + * @see `Vector3.DistanceSquared` + * @see `Vector3.neg` + * @see `Vector3.Unit` + * @param g1 the geometric representation of a game entity + * @param g2 the geometric representation of a game entity + * @return the crude distance between the two geometric representations + */ + def distanceCheck(g1: Geometry3D, g2: Geometry3D): Float = { + val dir = Vector3.Unit(g2.center.asVector3 - g1.center.asVector3) + val point1 = g1.pointOnOutside(dir).asVector3 + val point2 = g2.pointOnOutside(Vector3.neg(dir)).asVector3 + Vector3.DistanceSquared(point1, point2) } } diff --git a/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index 47a755e7..63e34e28 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -33,12 +33,13 @@ class ZoneMap(val name: String) { var checksum: Long = 0 var zipLinePaths: List[ZipLinePath] = List() var cavern: Boolean = false - var environment: List[PieceOfEnvironment] = List() + var environment: List[PieceOfEnvironment] = List() private var linkTurretWeapon: Map[Int, Int] = Map() private var linkTerminalPad: Map[Int, Int] = Map() private var linkTerminalInterface: Map[Int, Int] = Map() private var linkDoorLock: Map[Int, Int] = Map() private var linkObjectBase: Map[Int, Int] = Map() + private var containsShuttle: List[Int] = List() private var buildings: Map[(String, Int, Int), FoundationBuilder] = Map() private var lattice: Set[(String, String)] = Set() @@ -116,6 +117,12 @@ class ZoneMap(val name: String) { linkTurretWeapon = linkTurretWeapon ++ Map(turretGuid -> weaponGuid) } + def shuttleBays: List[Int] = containsShuttle + + def linkShuttleToBay(shuttleBayGuid: Int): Unit = { + containsShuttle = containsShuttle :+ shuttleBayGuid + } + def latticeLink: Set[(String, String)] = lattice def addLatticeLink(source: String, target: String): Unit = { diff --git a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala index a70b1845..3c73b416 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala @@ -44,6 +44,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act vehicle.Actor = context.actorOf(Props(classOf[VehicleControl], vehicle), PlanetSideServerObject.UniqueActorName(vehicle)) } + sender() ! Zone.Vehicle.HasSpawned(zone, vehicle) case Zone.Vehicle.Despawn(vehicle) => ZoneVehicleActor.recursiveFindVehicle(vehicleList.iterator, vehicle) match { @@ -51,6 +52,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act vehicleList.remove(index) context.stop(vehicle.Actor) vehicle.Actor = Default.Actor + sender() ! Zone.Vehicle.HasDespawned(zone, vehicle) case None => ; sender() ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find") } diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 00b75335..6b93a923 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -401,16 +401,16 @@ object GamePacketOpcode extends Enumeration { case 0x50 => game.TargetingInfoMessage.decode case 0x51 => game.TriggerEffectMessage.decode case 0x52 => game.WeaponDryFireMessage.decode - case 0x53 => noDecoder(DroppodLaunchRequestMessage) + case 0x53 => game.DroppodLaunchRequestMessage.decode case 0x54 => game.HackMessage.decode - case 0x55 => noDecoder(DroppodLaunchResponseMessage) + case 0x55 => game.DroppodLaunchResponseMessage.decode case 0x56 => game.GenericObjectActionMessage.decode case 0x57 => game.AvatarVehicleTimerMessage.decode // 0x58 case 0x58 => game.AvatarImplantMessage.decode case 0x59 => noDecoder(UnknownMessage89) case 0x5a => game.DelayedPathMountMsg.decode - case 0x5b => noDecoder(OrbitalShuttleTimeMsg) + case 0x5b => game.OrbitalShuttleTimeMsg.decode case 0x5c => noDecoder(AIDamage) case 0x5d => game.DeployObjectMessage.decode case 0x5e => game.FavoritesRequest.decode diff --git a/src/main/scala/net/psforever/packet/PSPacket.scala b/src/main/scala/net/psforever/packet/PSPacket.scala index 36e116e0..acf2e4ae 100644 --- a/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/src/main/scala/net/psforever/packet/PSPacket.scala @@ -39,6 +39,9 @@ trait PlanetSideCryptoPacket extends PlanetSidePacket { def opcode: CryptoPacketOpcode.Type } +/** PlanetSide ResetSequence packets: self-contained? */ +trait PlanetSideResetSequencePacket extends PlanetSidePacket { } + /** PlanetSide packet type. Used in more complicated packet headers * * ResetSequence - Not sure what this is used for or if the name matches what it actually does diff --git a/src/main/scala/net/psforever/packet/PacketCoding.scala b/src/main/scala/net/psforever/packet/PacketCoding.scala index 9da785fc..84d1d35e 100644 --- a/src/main/scala/net/psforever/packet/PacketCoding.scala +++ b/src/main/scala/net/psforever/packet/PacketCoding.scala @@ -174,7 +174,11 @@ object PacketCoding { } case PacketType.Crypto => if (flags.secured && crypto.isEmpty) { - return Failure(Err("Unsupported packet type: crypto packets must be unencrypted")) + return Failure(Err("Unsupported packet type: secured crypto packets must be unencrypted")) + } + case PacketType.ResetSequence => + if (!flags.secured || crypto.isEmpty) { + return Failure(Err("Unsupported packet type: reset sequence packets must be encrypted")) } case _ => return Failure(Err(s"Unsupported packet type: ${flags.packetType.toString}")) @@ -185,7 +189,7 @@ object PacketCoding { case Successful(DecodeResult(value, _remainder)) => (value, _remainder.toByteVector) case Failure(e) => - return Failure(Err(s"Failed to parse packet sequence number: ${e.message}")) + return Failure(Err(s"Failed to parse ${flags.packetType} packet sequence number: ${e.message}")) } (flags.packetType, crypto) match { @@ -199,9 +203,13 @@ object PacketCoding { case (PacketType.Normal, None) if !flags.secured => decodePacket(payload).map(p => (p, sequence)) case (PacketType.Normal, None) => - Failure(Err(s"Cannot unmarshal encrypted packet without CryptoCoding")) + Failure(Err("Cannot unmarshal encrypted packet without a cipher")) + case (PacketType.ResetSequence, Some(_crypto)) => + val test = _crypto.decrypt(payload.drop(1)) + Failure(Err(s"ResetSequence not completely supported, but: $flags, $sequence, and $payload; decrypt: $test")) + case (ptype, _) => + Failure(Err(s"Cannot unmarshal $ptype packet at all")) } - } /** diff --git a/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala b/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala index 35a5c70e..addf15d7 100644 --- a/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala +++ b/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala @@ -2,7 +2,7 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire} import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} @@ -14,7 +14,7 @@ final case class CharacterCreateRequestMessage( name: String, headId: Int, voiceId: CharacterVoice.Value, - gender: CharacterGender.Value, + gender: CharacterSex, empire: PlanetSideEmpire.Value ) extends PlanetSideGamePacket { type Packet = CharacterCreateRequestMessage @@ -29,7 +29,7 @@ object CharacterCreateRequestMessage extends Marshallable[CharacterCreateRequest ("name" | PacketHelpers.encodedWideString) :: ("headId" | uint8L) :: ("voiceId" | character_voice_codec) :: - ("gender" | CharacterGender.codec) :: + ("gender" | CharacterSex.codec) :: ("empire" | PlanetSideEmpire.codec) ).exmap[CharacterCreateRequestMessage]( { diff --git a/src/main/scala/net/psforever/packet/game/DroppodFreefallingMessage.scala b/src/main/scala/net/psforever/packet/game/DroppodFreefallingMessage.scala index c683e8ed..aa57a4d5 100644 --- a/src/main/scala/net/psforever/packet/game/DroppodFreefallingMessage.scala +++ b/src/main/scala/net/psforever/packet/game/DroppodFreefallingMessage.scala @@ -3,10 +3,32 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} import net.psforever.types.{Angular, PlanetSideGUID, Vector3} +import scodec.Attempt.Successful import scodec.Codec import scodec.codecs._ import shapeless.{::, HNil} +/** + * Dispatched by the server to trigger a droppod's traditional behavior of plummeting from lower orbit like a rock and + * slowing to a gentle land, breaking apart like flower petals to introduce a soldier to the battlefield.
+ *
+ * Only works on droppod-type vehicles. + * Only works if a client avatar is mounted in the vehicle. + * The furthest the vehicle will fall is determined by that avatar player's interaction with the ground. + * The camera is maneuvered in three ways - + * where it starts, + * where it tracks the falling vehicle, + * where it zooms in upon landing. + * Only the "where it starts" portion of the camera is slightly manipulable. + * @param guid the global unique identifier of the droppod + * @param pos the position of the droppod + * @param vel how quickly the droppod is moving + * @param pos2 suggestion for positioning external viewpoint while observing the droppod descending; + * the most common offset from the model position was `Vector3(-20, 1.156f, -50)` + * @param orientation1 na; + * the y-component is usually 70.3125f + * @param orientation2 na + */ final case class DroppodFreefallingMessage( guid: PlanetSideGUID, pos: Vector3, @@ -21,25 +43,23 @@ final case class DroppodFreefallingMessage( } object DroppodFreefallingMessage extends Marshallable[DroppodFreefallingMessage] { + private val rotation: Codec[Vector3] = ( + Angular.codec_roll :: + Angular.codec_pitch :: + Angular.codec_yaw() + ).narrow[Vector3]( + { + case u :: v :: w :: HNil => Successful(Vector3(u, v, w)) + }, + v => v.x :: v.y :: v.z :: HNil + ) + implicit val codec: Codec[DroppodFreefallingMessage] = ( ("guid" | PlanetSideGUID.codec) :: - ("pos" | Vector3.codec_float) :: - ("vel" | Vector3.codec_float) :: - ("pos2" | Vector3.codec_float) :: - ("unkA" | Angular.codec_roll) :: - ("unkB" | Angular.codec_pitch) :: - ("unkC" | Angular.codec_yaw()) :: - ("unkD" | Angular.codec_roll) :: - ("unkE" | Angular.codec_pitch) :: - ("unkF" | Angular.codec_yaw()) - ).xmap[DroppodFreefallingMessage]( - { - case guid :: pos :: vel :: pos2 :: uA :: uB :: uC :: uD :: uE :: uF :: HNil => - DroppodFreefallingMessage(guid, pos, vel, pos2, Vector3(uA, uB, uC), Vector3(uD, uE, uF)) - }, - { - case DroppodFreefallingMessage(guid, pos, vel, pos2, Vector3(uA, uB, uC), Vector3(uD, uE, uF)) => - guid :: pos :: vel :: pos2 :: uA :: uB :: uC :: uD :: uE :: uF :: HNil - } - ) + ("pos" | Vector3.codec_float) :: + ("vel" | Vector3.codec_float) :: + ("pos2" | Vector3.codec_float) :: + ("orientation1" | rotation) :: + ("orientation2" | rotation) + ).as[DroppodFreefallingMessage] } diff --git a/src/main/scala/net/psforever/packet/game/DroppodLaunchInfo.scala b/src/main/scala/net/psforever/packet/game/DroppodLaunchInfo.scala new file mode 100644 index 00000000..72e73d22 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/DroppodLaunchInfo.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.Attempt.Successful +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Information related to this droppod event. + * @see `DroppodLaunchRequestMessage` + * @see `DroppodLaunchResponseMessage` + * @param guid the player using the droppod + * @param zone_number the zone to which the player desires transportation + * @param xypos where in the zone (relative to the ground) the player will be placed + */ +final case class DroppodLaunchInfo( + guid: PlanetSideGUID, + zone_number: Int, + xypos: Vector3 + ) + +object DroppodLaunchInfo { + val codec: Codec[DroppodLaunchInfo] = ( + ("guid" | PlanetSideGUID.codec) :: + ("zone_number" | uint16L) :: + (floatL :: floatL).narrow[Vector3]( + { + case x :: y :: HNil => Successful(Vector3(x, y, 0)) + }, + { + case Vector3(x, y, _) => x :: y :: HNil + } + ) + ).as[DroppodLaunchInfo] +} diff --git a/src/main/scala/net/psforever/packet/game/DroppodLaunchRequestMessage.scala b/src/main/scala/net/psforever/packet/game/DroppodLaunchRequestMessage.scala new file mode 100644 index 00000000..bfab848b --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/DroppodLaunchRequestMessage.scala @@ -0,0 +1,47 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched from the client to indicate the player wishes to use an orbital droppod + * to rapidly deploy into a zone at a pre-approved position.
+ *
+ * Follows after an instance of "player stasis" where they are permitted to make this sort of selection + * by referencing a zone from the interstellar deployment map. + * This is the conclusion of utilizing the high altitude rapid transport (HART) system + * though does not need to be limited only to prior use of the orbital shuttle. + * @see `PlayerStasisMessage` + * @param info information related to this droppod event + * @param unk na; + * consistently 3 + */ +final case class DroppodLaunchRequestMessage( + info: DroppodLaunchInfo, + unk: Int + ) extends PlanetSideGamePacket { + type Packet = DroppodLaunchRequestMessage + def opcode = GamePacketOpcode.DroppodLaunchRequestMessage + def encode = DroppodLaunchRequestMessage.encode(this) +} + +object DroppodLaunchRequestMessage extends Marshallable[DroppodLaunchRequestMessage] { + /** + * Overloaded constructor that ignores the last field. + * Existing fields match `DroppodLaunchInfo`. + * @param guid the player using the droppod + * @param zoneNumber the zone to which the player desires transportation + * @param pos where in the zone (relative to the ground) the player will be placed + * @return a `DroppodLaunchRequestMessage` packet + */ + def apply(guid: PlanetSideGUID, zoneNumber: Int, pos: Vector3): DroppodLaunchRequestMessage = + DroppodLaunchRequestMessage(DroppodLaunchInfo(guid, zoneNumber, pos), 3) + + implicit val codec: Codec[DroppodLaunchRequestMessage] = ( + DroppodLaunchInfo.codec :: + ("unk" | uint2) + ).as[DroppodLaunchRequestMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/DroppodLaunchResponseMessage.scala b/src/main/scala/net/psforever/packet/game/DroppodLaunchResponseMessage.scala new file mode 100644 index 00000000..10bc1038 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/DroppodLaunchResponseMessage.scala @@ -0,0 +1,178 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import enumeratum.values.{IntEnum, IntEnumEntry} +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.Codec +import scodec.codecs._ + +/** + * The types of errors that can be reported when attempting to droppod into a zone.
+ *
+ * All codes show the preceding text in the events chat window. + * The typo in the message from `BlockedBySOI` can not be resolved by populating any of the greater packet's fields. + * `ZoneFullWarpQueue` utilizes the additional packet fields to establish the warp queue prompt + * with the warp queue and the player's position in that queue. + * The zone to which the player desires transportation is defined elsewhere in the greater packet. + */ +sealed abstract class DroppodError(val value: Int, val message: String) extends IntEnumEntry + +object DroppodError extends IntEnum[DroppodError] { + val values = findValues + + case object ContinentNotAvailable extends DroppodError( + value = 1, + message = "That continent is not available - please choose another one." + ) + + case object BlockedBySOI extends DroppodError( + value = 2, + message = "That location is within a 's Sphere of Influence (SOI). Please try another location." //typo intentional + ) + + case object InvalidLocation extends DroppodError( + value = 3, + message = "That is an invalid drop location - please try another location." + ) + + case object ZoneNotAvailable extends DroppodError( + value = 4, + message = "This zone is not available - try another zone." + ) + + case object ZoneFull extends DroppodError( + value = 5, + message = "That zone is already full of battle hungry people - try another one." + ) + + case object EnemyBase extends DroppodError( + value = 6, + message = "You can not drop onto an enemy home base - please choose a valid continent." + ) + + case object NotOnHart extends DroppodError( + value = 7, + message = "You are attempting to drop but are not on the HART - be warned you are being watched." + ) + + case object OwnFactionLocked extends DroppodError( + value = 8, + message = "You cannot drop onto a continent that is locked to your empire - please choose a valid continent." + ) + + case object ZoneFullWarpQueue extends DroppodError( + value = 9, + message = "The zone you are trying to warp to is currently full. You have been placed in the warp queue." + ) +} + +/** + * Information displayed on the zone warp queue in terms of queue size and queue progression. + * @param queue_size the number of players trying to warp to this zone in the queue ('b' if a/b) + * @param place the player's spot in the queue ('a' if a/b) + */ +final case class WarpQueuePrompt(queue_size: Long, place: Long) + +/** + * Dispatched from the client to indicate the player wished to use an orbital droppod + * but the player will be denied that request for a specific reason. + * The reason manifests as text appended to the event chat window. + * Occasionally, a supplemental window will open with additional information about a delayed action (warp queue). + * @see `DroppodLaunchInfo` + * @param error_code the error reporting why the zoning through droppod use failed + * @param launch_info information related to this droppod event + * @param queue_info if the error invokes the warp queue, the current information about the state of the queue + * @throws AssertionError if the error code requires additional fields + */ +final case class DroppodLaunchResponseMessage( + error_code: DroppodError, + launch_info: DroppodLaunchInfo, + queue_info: Option[WarpQueuePrompt] + ) extends PlanetSideGamePacket { + assert( + error_code != DroppodError.ZoneFullWarpQueue || queue_info.isDefined, + "ZoneFullWarpQueue requires queue information" + ) + type Packet = DroppodLaunchResponseMessage + def opcode = GamePacketOpcode.DroppodLaunchResponseMessage + def encode = DroppodLaunchResponseMessage.encode(this) +} + +object DroppodLaunchResponseMessage extends Marshallable[DroppodLaunchResponseMessage] { + /** + * Overloaded constructor for most errors. + * @param error the error reporting why the zoning through droppod use failed + * @param guid the player using the droppod + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(error: DroppodError, guid: PlanetSideGUID): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage(error, guid, 0, Vector3.Zero) + } + + /** + * Overloaded constructor for most errors. + * @param error the error reporting why the zoning through droppod use failed + * @param guid the player using the droppod + * @param zoneNumber the zone to which the player desires transportation + * @param xypos where in the zone (relative to the ground) the player will be placed + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(error: DroppodError, guid: PlanetSideGUID, zoneNumber: Int, xypos: Vector3): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage(error, DroppodLaunchInfo(guid, zoneNumber, xypos)) + } + + /** + * Overloaded constructor for quickly reflecting errors. + * @param error the error reporting why the zoning through droppod use failed + * @param info information related to this droppod event + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(error: DroppodError, info: DroppodLaunchInfo): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage(error, info, None) + } + + /** + * Overloaded constructor for `ZoneFullWarpQueue` errors. + * @param guid the player using the droppod + * @param zoneNumber the zone to which the player desires transportation + * @param queueSize the number of players trying to warp to this zone in the queue ('b' if a/b) + * @param placeInQueue the player's spot in the queue ('a' if a/b) + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(guid: PlanetSideGUID, zoneNumber: Int, queueSize: Int, placeInQueue: Int): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage( + DroppodLaunchInfo(guid, zoneNumber, Vector3.Zero), + queueSize, placeInQueue + ) + } + + /** + * Overloaded constructor for quickly reflecting `ZoneFullWarpQueue` errors. + * @param info information related to this droppod event + * @param queueSize the number of players trying to warp to this zone in the queue ('b' if a/b) + * @param placeInQueue the player's spot in the queue ('a' if a/b) + * @return a `DroppodLaunchResponseMessage` packet + */ + def apply(info: DroppodLaunchInfo, queueSize: Int, placeInQueue: Int): DroppodLaunchResponseMessage = { + DroppodLaunchResponseMessage( + DroppodError.ZoneFullWarpQueue, + info, + Some(WarpQueuePrompt(queueSize, placeInQueue)) + ) + } + + private val droppodErrorCodec: Codec[DroppodError] = PacketHelpers.createIntEnumCodec(DroppodError, uint4) + + private val extra_codec: Codec[WarpQueuePrompt] = ( + ("place" | uint32L) :: + ("queue_size" | uint32L) + ).as[WarpQueuePrompt] + + implicit val codec: Codec[DroppodLaunchResponseMessage] = ( + ("error_code" | droppodErrorCodec) >>:~ { ecode => + ("launch_info" | DroppodLaunchInfo.codec) :: + ("queue_info" | conditional(ecode == DroppodError.ZoneFullWarpQueue, extra_codec)) + } + ).as[DroppodLaunchResponseMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala index c0a920a4..2e8c19d6 100644 --- a/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala @@ -42,6 +42,7 @@ import scodec.codecs._ * 16 - Max unanchor * 20 - Client requests MAX special effect (NC shield and TR overdrive. VS jump jets are handled by the jump_thrust boolean on PlayerStateMessageUpstream) * 21 - Disable MAX special effect (NC shield) + * 28 - Cancel warp queue (see: `DroppodLaunchResponseMessage`)
* 29 - AFK
* 30 - back in game
* 36 - turn on "Looking for Squad"
diff --git a/src/main/scala/net/psforever/packet/game/MountVehicleMsg.scala b/src/main/scala/net/psforever/packet/game/MountVehicleMsg.scala index 2084239c..5f990109 100644 --- a/src/main/scala/net/psforever/packet/game/MountVehicleMsg.scala +++ b/src/main/scala/net/psforever/packet/game/MountVehicleMsg.scala @@ -12,13 +12,13 @@ import scodec.codecs._ * The client will only dispatch this packet when it feels confident that the player can get into a vehicle. * It makes its own check whether or not to display that "enter vehicle here" icon on the ground. * This is called an "entry point." - * Entry points and seat numbers are not required as one-to-one; - * multiple entry points can lead to the same seat, such as the driver seat of an ANT.
+ * Entry points and mount numbers are not required as one-to-one; + * multiple entry points can lead to the same mount, such as the driver mount of an ANT.
*
* The player is not allowed to board anything until the server responds in affirmation. * @param player_guid the player * @param vehicle_guid the vehicle - * @param entry_point the entry index that maps to a seat index, specific to the selected vehicle + * @param entry_point the entry index that maps to a mount index, specific to the selected vehicle */ final case class MountVehicleMsg(player_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID, entry_point: Int) extends PlanetSideGamePacket { diff --git a/src/main/scala/net/psforever/packet/game/OrbitalShuttleTimeMsg.scala b/src/main/scala/net/psforever/packet/game/OrbitalShuttleTimeMsg.scala new file mode 100644 index 00000000..f02d1010 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/OrbitalShuttleTimeMsg.scala @@ -0,0 +1,125 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.{HartSequence, PlanetSideGUID} +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Paired globally unique identifier numbers, + * the first one being the pad (`obbasemesh`) of a HART shuttle building, + * the second being the shuttle itself. + * @param pad the HART shuttle pad + * @param shuttle the HART orbital shuttle + * @param unk a control code; + * has indeterminate purpose regardless of the phase expressed in the greater packet; + * frequently `20` but also frequently varies + */ +final case class PadAndShuttlePair(pad: PlanetSideGUID, shuttle: PlanetSideGUID, unk: Int) + +/** + * Control the animation state transitions of the high altitude rapid transport (HART) orbital shuttle building + * and the accompanying orbital shuttle model.
+ *
+ * The animation sequence is controlled primarily by the first field and + * goes through a strict cycle of boarding, lift shuttle, takeoff, land, lower shuttle. + * All HART facilities (amenity `obbasemesh`) in a given zone are controlled by this packet. + * Multiple systems are controlled by a single field during a given animation, + * e.g., the boarding gantries are retracted or extended during the same part where the shuttle is raised or lowered. + * Certain neutral animation states - `State0`, `State5`, and `State7` - all behave the same way + * though denote different points in the sequence. + * Animation subsequence states are coordinated by the second field, + * though the specific purpose of the subsequence isn't always obvious, + * and the field isn't always necessary to achieve the result of the primary sequence.
+ *
+ * The total time of the system is bound between two states: + * whether the shuttle has left or whether it is boarding. + * When separated ("has left"), + * the shuttle will be lifted out of the bay to atop the building and will fly off into the horizon, + * remaining despawned until it returns to view, perches atop the building again, and is lowered into the bay. + * When boarding, + * the shuttle is fixed in the bay and is accepting passengers via one of the boarding hallways. + * Upon boarding the shuttle, the time until takeoff ("has left") is displayed to all waiting passengers + * in the form of a progress bar. + * This progress bar is fixed to a full time of 60 seconds (60000 milliseconds) in the client and + * will start at fractions of completion for boarding times under 60 seconds.
+ *
+ * Pairs of globally unique identifiers for the shuttle facility and the shuttle + * link the time fields to their function. + * All facilities and shuttles in a given zone are paired and enumerated for a single packet. + * If the HART facility identifier is missing or incorrect, + * the absent facility will continue to undergo correct animation state transition, + * but the door timer will not animate correctly and constantly display the time 10:37 and + * the door lights will be neither locked closed (red) or openable (green). + * If the shuttle identifier is missing or incorrect, + * the absent shuttle will continue to undergo partially correct animation state transitions, + * cycling between visible and invisible atop the HART facility, + * and the aforementioned progress bars visible by shuttle passengers will not display during the boarding phase + * if the shuttle is made available for boarding. + * @param model_state a control code that affects the over-all state of the HART system + * @param unk0 na + * @param arrival_time the time for the orbital shuttle to return during instances when the shuttle is away; + * displayed on a related time near the shuttle boarding entryways; + * in milliseconds + * @param boarding_time the time for the orbital shuttle to depart during instances when the shuttle is boarding; + * frequently `8000L` when not in use; + * in milliseconds + * @param other_time time field used for a variety of things; + * in most uses, the amount of time that has passed since the start of the event, + * so usually `0` (at start of event); + * with respects to `model_state` and `unk3`: + * full departure time when `5`-`3` (variant of `7`-`3`); + * occasionally, full departure time when `0`-`0` + * in milliseconds + * @param pairs a list of entries that pair + * a paired facility pad unique identifier and shuttle unique identifier + * with a control code + */ +final case class OrbitalShuttleTimeMsg( + model_state: HartSequence, + unk0: Int, + arrival_time: Long, + boarding_time: Long, + other_time: Long, + pairs: List[PadAndShuttlePair] + ) + extends PlanetSideGamePacket { + type Packet = OrbitalShuttleTimeMsg + def opcode = GamePacketOpcode.OrbitalShuttleTimeMsg + def encode = OrbitalShuttleTimeMsg.encode(this) +} + +object OrbitalShuttleTimeMsg extends Marshallable[OrbitalShuttleTimeMsg] { + private val uint3: Codec[Int] = uint(bits = 3) + + private val hartSequenceCodec: Codec[HartSequence] = PacketHelpers.createIntEnumCodec(HartSequence, uint3) + + private val padShuttlePair_codec: Codec[PadAndShuttlePair] = ( + ("pad" | PlanetSideGUID.codec) :: + ("shuttle" | PlanetSideGUID.codec) :: + ("unk" | uint(bits = 6)) + ).as[PadAndShuttlePair] + + implicit val codec: Codec[OrbitalShuttleTimeMsg] = ( + uint3 >>:~ { size => + ("model_state" | hartSequenceCodec) :: + ("unk0" | uint3) :: + ("arrival_time" | uint32L) :: + ("boarding_time" | uint32L) :: + bool :: + ("other_time" | uint32L) :: + ("pairs" | PacketHelpers.listOfNSized(size, padShuttlePair_codec)) + } + ).xmap[OrbitalShuttleTimeMsg]( + { + case _ :: model :: u0 :: arrival :: boarding :: _ :: other :: pairs :: HNil => + OrbitalShuttleTimeMsg(model, u0, arrival, boarding, other, pairs) + }, + { + case OrbitalShuttleTimeMsg(model, u0, arrival, boarding, other, pairs) => + pairs.length :: model :: u0 :: arrival :: boarding :: true :: other :: pairs :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index 02b02301..f4951b63 100644 --- a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -179,14 +179,14 @@ import scodec.codecs._ * `228 - Player/vehicle leaves black ops`
*
* `Vehicles:`
- * `10 - Driver seat permissions` + * `10 - Driver mount permissions` *
    *
  • 0 - Locked
  • *
  • 1 - Group
  • *
  • 3 - Empire
  • *
- * `11 - Gunner seat(s) permissions (same)`
- * `12 - Passenger seat(s) permissions (same)`
+ * `11 - Gunner mount(s) permissions (same)`
+ * `12 - Passenger mount(s) permissions (same)`
* `13 - Trunk permissions (same)`
* `21 - Declare a player the vehicle's owner, by globally unique identifier`
* `22 - Toggles gunner and passenger mount points (1 = hides, 0 = reveals; this also locks their permissions)`
diff --git a/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala b/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala index faccb900..a00d1ff8 100644 --- a/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/VehicleStateMessage.scala @@ -15,7 +15,9 @@ import scodec.codecs._ * @param ang the orientation of the vehicle * @param vel optional movement data * @param flying flight information, valid only for a vehicle that can fly when in flight; - * `Some(7)`, when in a flying state (vertical thrust unnecessary to unlock movement) + * `Some(7)`, when in a flying state (vertical thrust unnecessary to unlock movement); + * `Some(10) - Some(15)`, used by the HART during landing and take-off, + * 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 diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/BasicCharacterData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/BasicCharacterData.scala index 82047b17..ecb96742 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/BasicCharacterData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/BasicCharacterData.scala @@ -9,7 +9,7 @@ import net.psforever.types._ * This partition of the data stream contains information used to represent how the player's avatar is presented. * This appearance coincides with the data available from the `CharacterCreateRequestMessage` packet. * @see `PlanetSideEmpire`
- * `CharacterGender` + * `CharacterSex` * @param name the unique name of the avatar; * minimum of two characters * @param faction the empire to which the avatar belongs @@ -21,7 +21,7 @@ import net.psforever.types._ final case class BasicCharacterData( name: String, faction: PlanetSideEmpire.Value, - sex: CharacterGender.Value, + sex: CharacterSex, head: Int, voice: CharacterVoice.Value ) diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala index b4ea7d43..70a0ee16 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala @@ -304,7 +304,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { ("name" | PacketHelpers.encodedWideStringAligned(namePadding(name_padding, data.v2))) :: ("exosuit" | ExoSuitType.codec) :: ("unk5" | uint2) :: //unknown - ("sex" | CharacterGender.codec) :: + ("sex" | CharacterSex.codec) :: ("head" | uint8L) :: ("voice" | CharacterVoice.codec) :: ("unk6" | uint32L) :: diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/DroppodData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/DroppodData.scala index 98762166..470b59a8 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/DroppodData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/DroppodData.scala @@ -15,7 +15,7 @@ import shapeless.{::, HNil} * Upon hitting the ground, it opens up, releasing the player, and despawns.
*
* Although the droppod is not technically a vehicle, it is treated as such by the game. - * A spawned and unoccupied droppod can be entered and exited, as expected (the seat is 0). + * A spawned and unoccupied droppod can be entered and exited, as expected (the mount is 0). * There is no entry animation. * The exit animation is the droppod flowering open as usual. * Even in its spread open state, the droppod can be re-entered, though it will remain spread open. diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala index 44dcb218..2c311853 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala @@ -55,7 +55,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl * health should be less than 3/255, or 0%
* -jammered - vehicles will not be jammered by setting this field
* -player_guid the vehicle's (official) owner; - * a living player in the game world on the same continent as the vehicle who may mount the driver seat + * a living player in the game world on the same continent as the vehicle who may mount the driver mount * @param unk3 na * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) * @param unk4 na @@ -69,7 +69,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl * see `vehicle_type` * @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included; * will also include trunk contents; - * the driver is the only valid seat entry (more will cause the access permissions to act up) + * the driver is the only valid mount entry (more will cause the access permissions to act up) * @param vehicle_type a modifier for parsing the vehicle data format differently; * see `vehicle_format_data`; * defaults to `Normal` @@ -393,8 +393,8 @@ object VehicleData extends Marshallable[VehicleData] { * the entries are temporarily formatted into a linked list before being put back into a normal `List`.
*
* 6 June 2018:
- * Due to curious behavior in the vehicle seat access controls, - * please only encode and decode the driver seat even though all seats are currently reachable. + * Due to curious behavior in the vehicle mount access controls, + * please only encode and decode the driver mount even though all seats are currently reachable. * @param length the distance in bits to the first inventory entry * @return a `Codec` that translates `InventoryData` */ @@ -404,8 +404,8 @@ object VehicleData extends Marshallable[VehicleData] { uint8 >>:~ { size => uint2 :: (inventory_seat_codec( - length, //length of stream until current seat - CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next seat + length, //length of stream until current mount + CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next mount ) >>:~ { seats => PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist }) @@ -450,13 +450,13 @@ object VehicleData extends Marshallable[VehicleData] { conditional( objClass == ObjectClass.avatar, inventory_seat_codec( - { //length of stream until next seat + { //length of stream until next mount length + (seat match { case Some(o) => o.bitsize case None => 0 }) }, - CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next seat + CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next mount ) ).hlist } @@ -487,7 +487,7 @@ object VehicleData extends Marshallable[VehicleData] { * The operation performed by this `Codec` is very similar to `InternalSlot.codec`. * @param pad the padding offset for the player's name; * 0-7 bits; - * this padding value must recalculate for each represented seat + * this padding value must recalculate for each represented mount * @see `CharacterAppearanceData`
* `VehicleData.InitialStreamLengthToSeatEntries`
* `CumulativeSeatedPlayerNamePadding` diff --git a/src/main/scala/net/psforever/persistence/Avatar.scala b/src/main/scala/net/psforever/persistence/Avatar.scala index 082b05a1..24dae5f1 100644 --- a/src/main/scala/net/psforever/persistence/Avatar.scala +++ b/src/main/scala/net/psforever/persistence/Avatar.scala @@ -3,7 +3,7 @@ package net.psforever.persistence import net.psforever.objects.avatar import net.psforever.objects.avatar.Cosmetic import org.joda.time.LocalDateTime -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire} case class Avatar( id: Int, @@ -27,7 +27,7 @@ case class Avatar( id, name, PlanetSideEmpire(factionId), - CharacterGender(genderId), + CharacterSex.valuesToEntriesMap(genderId), headId, CharacterVoice(voiceId), bep, diff --git a/src/main/scala/net/psforever/services/InterstellarClusterService.scala b/src/main/scala/net/psforever/services/InterstellarClusterService.scala index c8ed4d95..3e6133e2 100644 --- a/src/main/scala/net/psforever/services/InterstellarClusterService.scala +++ b/src/main/scala/net/psforever/services/InterstellarClusterService.scala @@ -8,8 +8,10 @@ import net.psforever.objects.avatar.Avatar import net.psforever.objects.{Player, SpawnPoint, Vehicle} import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.zones.Zone +import net.psforever.packet.game.DroppodError import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3} import net.psforever.util.Config +import net.psforever.zones.Zones import scala.collection.mutable import scala.util.Random @@ -71,6 +73,19 @@ object InterstellarClusterService { final case class GetPlayers(replyTo: ActorRef[PlayersResponse]) extends Command final case class PlayersResponse(players: Seq[Avatar]) + + final case class DroppodLaunchRequest( + zoneNumber: Int, + position: Vector3, + faction: PlanetSideEmpire.Value, + replyTo: ActorRef[DroppodLaunchExchange] + ) extends Command + + trait DroppodLaunchExchange + + final case class DroppodLaunchConfirmation(destination: Zone, position: Vector3) extends DroppodLaunchExchange + + final case class DroppodLaunchDenial(errorCode: DroppodError, data: Option[Any]) extends DroppodLaunchExchange } class InterstellarClusterService(context: ActorContext[InterstellarClusterService.Command], _zones: Iterable[Zone]) @@ -138,7 +153,7 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic Ordering[Int].reverse ) // greatest > least .sortWith { - case ((_, spot1, _), (_, spot2, _)) => + case ((_, spot1, _), (_, _, _)) => spot1.ActivityBy().contains(faction) // prefer own faction activity } .headOption @@ -210,6 +225,22 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic case None => replyTo ! SpawnPointResponse(None) } + + case DroppodLaunchRequest(zoneNumber, position, faction, replyTo) => + zones.find(_.Number == zoneNumber) match { + case Some(zone) => + //TODO all of the checks for the specific DroppodLaunchResponseMessage excuses go here + if(zone.map.cavern) { + //just being cautious - caverns are typically not normally selectable as drop zones + replyTo ! DroppodLaunchDenial(DroppodError.ZoneNotAvailable, None) + } else if (zone.Number == Zones.sanctuaryZoneNumber(faction)) { + replyTo ! DroppodLaunchDenial(DroppodError.OwnFactionLocked, None) + } else { + replyTo ! DroppodLaunchConfirmation(zone, position) + } + case None => + replyTo ! DroppodLaunchDenial(DroppodError.InvalidLocation, None) + } } this diff --git a/src/main/scala/net/psforever/services/account/AccountIntermediaryService.scala b/src/main/scala/net/psforever/services/account/AccountIntermediaryService.scala index ea619aca..8db0d20a 100644 --- a/src/main/scala/net/psforever/services/account/AccountIntermediaryService.scala +++ b/src/main/scala/net/psforever/services/account/AccountIntermediaryService.scala @@ -25,37 +25,33 @@ class AccountIntermediaryService extends Actor { private val IPAddressBySessionID = mutable.Map[Long, IPAddress]() private[this] val log = org.log4s.getLogger - override def preStart() = { - log.trace("Starting...") - } - def receive = { // Called by the LoginSessionActor case StoreAccountData(token, account) => accountsByToken += (token -> account) - log.info(s"Storing intermediary account data for ${account.id}") + log.trace(s"Storing intermediary account data for ${account.id}") // Called by the WorldSessionActor case RetrieveAccountData(token) => accountsByToken.remove(token) match { case Some(acc) => sender() ! ReceiveAccountData(acc) - log.info(s"Retrieving intermediary account data for $acc") + log.trace(s"Retrieving intermediary account data for $acc") case None => log.error(s"Unable to retrieve intermediary account data for $token") } case StoreIPAddress(sessionID, address) => IPAddressBySessionID += (sessionID -> address) - log.info(s"Storing IP address (${address.Address}) for sessionID : $sessionID") + log.trace(s"Storing IP address (${address.Address}) for sessionID : $sessionID") case RetrieveIPAddress(sessionID) => val address: Option[IPAddress] = IPAddressBySessionID.remove(sessionID) if (address.nonEmpty) { sender() ! ReceiveIPAddress(address.get) - log.info(s"Retrieving IP address data for sessionID : ${sessionID}") + log.trace(s"Retrieving IP address data for sessionID : $sessionID") } else { - log.error(s"Unable to retrieve IP address data for sessionID : ${sessionID}") + log.error(s"Unable to retrieve IP address data for sessionID : $sessionID") } case msg => diff --git a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala index 1f0c2afb..b314a69c 100644 --- a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala +++ b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala @@ -21,13 +21,13 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} * relogging (short-term client connectivity issue resolution), and * logout (end-of-life conditions involving the separation of a user from the game world).
*
- * A user polls this service and the net.psforever.services either creates a new `PersistenceMonitor` entity + * A user polls this service and the service either creates a new `PersistenceMonitor` entity * or returns whatever `PersistenceMonitor` entity currently exists. - * Performing informative pdates to the monitor about the user's eventual player avatar instance + * Performing informative updates to the monitor about the user's eventual player avatar instance * (which can be performed by messaging the service indirectly, * though sending directly to the monitor is recommended) * facilitate the management of persistence. - * If connectivity isssues with the client are encountered by the user, + * If connectivity issues with the client are encountered by the user, * within a reasonable amount of time to connection restoration, * the user may regain control of their existing persistence monitor and, thus, the same player avatar. * End of life is mainly managed by the monitors internally @@ -59,7 +59,6 @@ class AccountPersistenceService extends Actor { */ override def preStart(): Unit = { ServiceManager.serviceManager ! ServiceManager.Lookup("squad") - log.trace("Awaiting system service hooks ...") } override def postStop(): Unit = { @@ -144,7 +143,7 @@ class AccountPersistenceService extends Actor { */ def CreateNewPlayerToken(name: String): ActorRef = { val ref = - context.actorOf(Props(classOf[PersistenceMonitor], name, squad), s"$name-${NextPlayerIndex(name)}") + context.actorOf(Props(classOf[PersistenceMonitor], name, squad), s"${NextPlayerIndex(name)}_${name.hashCode()}") accounts += name -> ref ref } @@ -312,7 +311,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor { *
* The updates have been providing the zone * and the basic information about the user (player name) has been provided since the beginning - * and it's a trivial matter to find where the avatar and player and asess their circumstances. + * and it's a trivial matter to find where the avatar and player and assess their circumstances. * The four important vectors are: * the player avatar is in a vehicle, * the player avatar is standing, @@ -336,7 +335,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor { case _ => (None, None) //bad data? }) match { case (Some(_), Some(seat)) => - seat.Occupant = None //unseat + seat.unmount(player) //unmount case _ => ; } PlayerAvatarLogout(avatar, player) @@ -410,7 +409,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor { Deployables.Disown(inZone, avatar, context.parent) inZone.Population.tell(Zone.Population.Leave(avatar), context.parent) inZone.tasks.tell(GUIDTask.UnregisterObjectTask(avatar.locker)(inZone.GUID), context.parent) - log.info(s"logout of ${avatar.name}") + log.info(s"Logout of ${avatar.name}") } } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index 8b9a6e5c..d02a6824 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -15,17 +15,12 @@ class AvatarService(zone: Zone) extends Actor { private[this] val log = org.log4s.getLogger - override def preStart() = { - log.trace(s"Awaiting ${zone.id} avatar events ...") - } - val AvatarEvents = new GenericEventBus[AvatarServiceResponse] //AvatarEventBus def receive = { case Service.Join(channel) => val path = s"/$channel/Avatar" val who = sender() - log.info(s"$who has joined $path") AvatarEvents.subscribe(who, path) case Service.Leave(None) => @@ -33,8 +28,6 @@ class AvatarService(zone: Zone) extends Actor { case Service.Leave(Some(channel)) => val path = s"/$channel/Avatar" - val who = sender() - log.info(s"$who has left $path") AvatarEvents.unsubscribe(sender(), path) case Service.LeaveAll() => diff --git a/src/main/scala/net/psforever/services/chat/ChatService.scala b/src/main/scala/net/psforever/services/chat/ChatService.scala index 36ad052b..9bbcd115 100644 --- a/src/main/scala/net/psforever/services/chat/ChatService.scala +++ b/src/main/scala/net/psforever/services/chat/ChatService.scala @@ -142,7 +142,7 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe ) case (None, _, _, _) => - log.error("received message from non-subscribed actor") + log.warn("received message from non-subscribed actor") } diff --git a/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala b/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala index be6e6cb2..2f2eeb07 100644 --- a/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala +++ b/src/main/scala/net/psforever/services/galaxy/GalaxyService.scala @@ -9,38 +9,26 @@ import net.psforever.services.{GenericEventBus, Service} class GalaxyService extends Actor { private[this] val log = org.log4s.getLogger - override def preStart() = { - log.info("Starting...") - } - val GalaxyEvents = new GenericEventBus[GalaxyServiceResponse] def receive: Receive = { case Service.Join(faction) if "TRNCVS".containsSlice(faction) => val path = s"/$faction/Galaxy" - val who = sender() - log.trace(s"$who has joined $path") - GalaxyEvents.subscribe(who, path) + GalaxyEvents.subscribe(sender(), path) case Service.Join("galaxy") => val path = s"/Galaxy" - val who = sender() - log.trace(s"$who has joined $path") - GalaxyEvents.subscribe(who, path) + GalaxyEvents.subscribe(sender(), path) case Service.Join(channel) => val path = s"/$channel/Galaxy" - val who = sender() - log.trace(s"$who has joined $path") - GalaxyEvents.subscribe(who, path) + GalaxyEvents.subscribe(sender(), path) case Service.Leave(None) => GalaxyEvents.unsubscribe(sender()) case Service.Leave(Some(channel)) => val path = s"/$channel/Galaxy" - val who = sender() - log.trace(s"$who has left $path") GalaxyEvents.unsubscribe(sender(), path) case Service.LeaveAll() => @@ -58,7 +46,7 @@ class GalaxyService extends Actor { GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.FlagMapUpdate(msg)) ) - case GalaxyAction.TransferPassenger(player_guid, temp_channel, vehicle, vehicle_to_delete, manifest) => + case GalaxyAction.TransferPassenger(_, temp_channel, vehicle, vehicle_to_delete, manifest) => GalaxyEvents.publish( GalaxyServiceResponse( s"/$forChannel/Galaxy", @@ -74,6 +62,6 @@ class GalaxyService extends Actor { ) case msg => - log.info(s"Unhandled message $msg from ${sender()}") + log.warn(s"Unhandled message $msg from ${sender()}") } } diff --git a/src/main/scala/net/psforever/services/hart/HartEvent.scala b/src/main/scala/net/psforever/services/hart/HartEvent.scala new file mode 100644 index 00000000..175c8c1d --- /dev/null +++ b/src/main/scala/net/psforever/services/hart/HartEvent.scala @@ -0,0 +1,272 @@ +// Copyright (c) 2021 PSForever +package net.psforever.services.hart + +import net.psforever.types.HartSequence + +/** + * The various `flying` states assigned to the orbital shuttle + * in close to an order in which they are assigned. + */ +object ShuttleState extends Enumeration { + type Type = Value + + val State13 = Value(13) + val State14 = Value(14) + val State10 = Value(10) + val State11 = Value(11) + val State12 = Value(12) + val State15 = Value(15) +} + +/** + * Produce the specific animation sequence and the ???. + * @see `OrbitalShuttleEvent` + * @see `OrbitalShuttleTimeMsg` + * @see `HartEvent` + * @param u1 the animation code for the HART + * @param u2 ??? + */ +final case class HartEventStateFields(u1: HartSequence, u2: Int) + +/** + * Produce the time data of this event in the sequence. + * @see `OrbitalShuttleEvent` + * @see `OrbitalShuttleTimeMsg` + * @see `HartEvent` + * @param t1 in general, time for the shuttle to arrive + * @param t2 in general, `8000L`; + * when being useful, time for the shuttle to board passengers + * @param t3 in general, time elasped + */ +final case class HartEventTimeFields(t1: Long, t2: Long, t3: Long) + +/** + * An event in the sequence of the high-altitude rapid transport (HART) system + * encompassing both ground facility conditions and conditions of the orbital shuttle. + */ +sealed trait HartEvent { + /** HART facility and shuttle animation */ + def u1: HartSequence + /** counter? */ + def u2: Int + /** starting time on the clock; typically seen on the display */ + def timeOnClock: Long + /** for how long this event goes on */ + def duration: Long + /** are the managed doors for the HART facility locked closed; + * this is an active state field: `true` - locked right now and `false` - unlocked right now + */ + def lockedDoors: Boolean = true + /** the shuttle has a unique state to expose to the zone; + * the state is related to a value in the `Flying` field of a `VehicleStateMessage` packet + */ + def shuttleState: Option[ShuttleState.Value] + /** how the shuttle and the HART facility interact; + * this is an active state field: + * `Some(true)` - the shuttle is docked right now; + * `Some(false)` - the shuttle has freed itself from the facility's dock right now; + * `None` - the shuttle is acting freely apart from its facility */ + def docked: Option[Boolean] + /** these fields must be including prior to an update if the shuttle state was not previous known; + * the primary purpose is to place the shuttle platform at the correct elevation + */ + def prerequisiteUpdate: Option[HartEventStateFields] + + /** + * Get the animation state fields for this event. + * @param time during update requests, the amount of time that has elapsed during the start of this event + * @return the animation state data + */ + def stateFields(time: Option[Long] = None): HartEventStateFields = { + HartEventStateFields(u1, u2) + } + + /** + * Get the primary time fields for this event. + * @param time during update requests, the amount of time that has elapsed during the start of this event + * @return the time data + */ + def timeFields(time: Option[Long] = None): HartEventTimeFields = { + HartEventTimeFields( + time match { + case Some(t) if timeOnClock > t => timeOnClock - t + case Some(t) if timeOnClock <= t => 0L + case _ => timeOnClock + }, + 8000L, + time match { + case Some(t) => t + case _ => 0 + } + ) + } +} + +object HartEvent { + private val prepareForDepartureOnUpdate: Option[HartEventStateFields] = + Some(HartEventStateFields(HartSequence.PrepareForDeparture, 1)) + + final case class Boarding(duration: Long) extends HartEvent { + def u1: HartSequence = HartSequence.State0 + def u2: Int = 0 + def timeOnClock: Long = duration + override def lockedDoors: Boolean = false + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State10) + def docked: Option[Boolean] = Some(true) + def prerequisiteUpdate: Option[HartEventStateFields] = None + + override def timeFields(time: Option[Long]): HartEventTimeFields = { + /* + the full progress bar only displays 60s + for other times, the progress bar will only display the portion necessary to represent the time in respect to 60s + */ + HartEventTimeFields( + 0L, + super.timeFields(time).t1, + time match { + case None => 0L + case Some(_) => timeOnClock + } + ) + } + } + + final case class ShuttleTakeoffOps(timeOnClock: Long) extends HartEvent { + def u1: HartSequence = HartSequence.PrepareForDeparture + def u2: Int = 1 + def duration: Long = 8000 + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State11) + def docked: Option[Boolean] = Some(true) + def prerequisiteUpdate: Option[HartEventStateFields] = None + } + + object ShuttleTakeoffOps { + final val duration: Long = 8000L + } + + final case class Takeoff(timeOnClock: Long) extends HartEvent { + def u1: HartSequence = HartSequence.TakeOff + def u2: Int = 2 + def duration: Long = Takeoff.duration + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State12) + def docked: Option[Boolean] = Some(false) + def prerequisiteUpdate: Option[HartEventStateFields] = prepareForDepartureOnUpdate + } + + object Takeoff { + final val duration: Long = 13300L + } + + final case class InTransit( + timeOnClock: Long, + duration: Long, + boardingDuration: Long + ) extends HartEvent { + def u1: HartSequence = HartSequence.State7 + def u2: Int = 3 + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State15) + def docked: Option[Boolean] = None + def prerequisiteUpdate: Option[HartEventStateFields] = None + + override def stateFields(time: Option[Long] = None): HartEventStateFields = { + HartEventStateFields( + time match { + case Some(_) => HartSequence.State5 + case _ => u1 + }, + u2 + ) + } + + override def timeFields(time: Option[Long]): HartEventTimeFields = { + HartEventTimeFields( + time match { + case Some(t) if timeOnClock > t => timeOnClock - t + case Some(t) if timeOnClock <= t => 0L + case _ => timeOnClock + }, + 8000L, + time match { + case Some(_) => boardingDuration + case _ => 0 + } + ) + } + } + + case object Arrival extends HartEvent { + def u1: HartSequence = HartSequence.Land + def u2: Int = 4 + def timeOnClock: Long = 23700 + def duration: Long = 15700 + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State13) + def docked: Option[Boolean] = None + def prerequisiteUpdate: Option[HartEventStateFields] = prepareForDepartureOnUpdate + } + + case object ShuttleDockingOps extends HartEvent { + def u1: HartSequence = HartSequence.PrepareForBoarding + def u2: Int = 5 + def timeOnClock: Long = 8000 + def duration: Long = 8000 + def shuttleState: Option[ShuttleState.Value] = Some(ShuttleState.State14) + def docked: Option[Boolean] = Some(true) + def prerequisiteUpdate: Option[HartEventStateFields] = None + } + + case object Blanking extends HartEvent { + def u1: HartSequence = HartSequence.State0 + def u2: Int = 5 + def timeOnClock: Long = 4294967295L + def duration: Long = 1 //for how long? + def shuttleState: Option[ShuttleState.Value] = None + def docked: Option[Boolean] = Some(true) + def prerequisiteUpdate: Option[HartEventStateFields] = None + + override def timeFields(time: Option[Long]): HartEventTimeFields = + HartEventTimeFields(timeOnClock, 8000L, 0L) + } + + /** + * The high alititude rapid transport (HART) system is centered around a series of animations + * of a component orbital shuttle landing and taking off from a given facility. + * The two important times are the length pof the time the shuttle is away from the facility and + * the length of time that the shuttle is docked at the facility to allow for passenger boarding. + * The sequence progresses through stages from the shuttle being landed, to the shuttle departing, + * to the shuttle returning, and then starting back with the shuttle being landed. + *
+ * As the shuttle animates, the facility also animates. + * As both the shuttle and the facility animate, various other components connect to the facility and to the shuttle + * undergo state changes, allowing or denying access to the shuttle's boarding routines. + * When boarding is permitted, this phase is considered as part of a single event in the sequence, + * and boarding duration lasts for that entire event. + * The remainder of the sequence is devoted to a remainder of time from the other duration + * once the known time of fixed animation events are deducted. + * @param inFlightDuration for how long the orbital shuttle is away from being docked at the HART building + * and not allowing passengers to board + * @param boardingDuration for how long the orbital shuttle is landed at its component HART building + * and is allowing passnegers to board + * @return the final sequence of events + */ + def buildEventSequence(inFlightDuration: Long, boardingDuration: Long): Seq[HartEvent] = { + val returnDurations = Arrival.duration + ShuttleDockingOps.duration + val fixedDurations = ShuttleTakeoffOps.duration + Takeoff.duration + returnDurations + val full = if (inFlightDuration > fixedDurations) { + inFlightDuration + } else { + inFlightDuration + fixedDurations + } + val firstTime = full - ShuttleTakeoffOps.duration + val secondTime = firstTime - Takeoff.duration + val awayDuration = secondTime - returnDurations + Seq( + Boarding(boardingDuration), + ShuttleTakeoffOps(full), + Takeoff(firstTime), + InTransit(secondTime, awayDuration, boardingDuration), + Arrival, + ShuttleDockingOps, + Blanking + ) + } +} diff --git a/src/main/scala/net/psforever/services/hart/HartService.scala b/src/main/scala/net/psforever/services/hart/HartService.scala new file mode 100644 index 00000000..4ecbfb77 --- /dev/null +++ b/src/main/scala/net/psforever/services/hart/HartService.scala @@ -0,0 +1,50 @@ +// Copyright (c) 2021 PSForever +package net.psforever.services.hart + +import akka.actor.{Actor, ActorRef, Props} +import net.psforever.util.Config + +import scala.collection.concurrent.TrieMap + +/** + * Coordinate the components - facility landing pad and orbital shuttle - + * of the high altitude rapid transport (HART) system for any zone that attempts to register. + * When a pair of staging pad and orbital shuttle attempt to register with the system, + * either locate an existing zone-based manager or create a new manager for this zone, + * and tell that manager that the pair is (now) under its supervision. + * @see `HartTimer` + */ +class HartService extends Actor { + /** key - a zone id; value - the manager for that zone's HART system */ + val zoneTimers: TrieMap[String, ActorRef] = TrieMap[String, ActorRef]() + + def receive: Receive = { + case out : HartTimer.PairWith => + val zone = out.zone + val channel = zone.id + (zoneTimers.get(channel) match { + case Some(o) => + o + case None => + val actor = context.actorOf(Props(classOf[HartTimer], zone), s"$channel-shuttle-timer") + zoneTimers.put(channel, actor) + actor.tell( + HartTimer.SetEventDurations( + channel, + Config.app.game.hart.inFlightDuration, + Config.app.game.hart.boardingDuration + ), + self + ) + actor + }).tell(out, out.from) + + case out: HartTimer.MessageToHartInZone => + zoneTimers.get(out.inZone) match { + case Some(o) => o ! out + case _ => + } + + case _ => ; + } +} diff --git a/src/main/scala/net/psforever/services/hart/HartTimer.scala b/src/main/scala/net/psforever/services/hart/HartTimer.scala new file mode 100644 index 00000000..3de884fe --- /dev/null +++ b/src/main/scala/net/psforever/services/hart/HartTimer.scala @@ -0,0 +1,286 @@ +// Copyright (c) 2021 PSForever +package net.psforever.services.hart + +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.objects.Default +import net.psforever.objects.zones.Zone +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.{GenericEventBus, GenericEventBusMsg} +import net.psforever.types.{HartSequence, PlanetSideGUID} + +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +/** + * Within each zone, all high-altitude rapid transport (HART) systems are controlled in unison. + * A HART system is composed of a facility (amenity) that embodies passenger onboarding services + * and a semi-interactive shuttle that gateways to the orbital droppod system. + * Provide supervision to these components by managing the over-all HART sequence. + * @param zone the zone being represented by this particular HART service + */ +class HartTimer(zone: Zone) extends Actor { + /** since the system is zone-locked, caching this value is fine */ + val zoneId = zone.id + /** all of the paired HART facility amenities and the shuttle housed in that facility (in that order) */ + var padAndShuttlePairs: List[(PlanetSideGUID, PlanetSideGUID)] = List() + + /* the HART system is controlled by a sequence of events; + * the sequence describes key state changes and animation cues + * to produce the effect of the orbital shuttle being used + */ + var sequence = Seq.empty[HartEvent] + /** index keeping track of the current event in the sequence */ + var sequenceIndex: Int = 0 + /** how many events are a part of this sequence */ + var sequenceLength = 0 + /** when the timing of the events in the system changes, + * do not push the changes until completion of the current routine + */ + var delayedScheduleChange: Option[Seq[HartEvent]] = None + /** the time at the start of the previous event */ + var lastStartTime: Long = 0 + /** scheduler for each event in the sequence */ + var timer: Cancellable = Default.Cancellable + + /** a message bus to which all associated orbital shuttle pads are subscribed */ + val padEvents = new GenericEventBus[HartTimer.Command] + /** cache common messages */ + val shuttleDockedInThisZone = HartTimer.ShuttleDocked(zoneId) + val shuttleFreeFromDockInThisZone = HartTimer.ShuttleFreeFromDock(zoneId) + + /** the behaviors common to both the inert and active operations of the hart */ + val commonBehavior: Receive = { + case HartTimer.SetEventDurations(_, awayDuration: Long, boardingDuration: Long) => + val newSequence = HartEvent.buildEventSequence(awayDuration, boardingDuration) + if (newSequence.nonEmpty) { + if (timer.isCancelled) { + sequence = newSequence + sequenceLength = newSequence.length + nextEvent(sequenceIndex) + } else { + delayedScheduleChange = Some(newSequence) + } + context.become(flightsScheduled) + } + } + + /** behaviors that are valid while no sequence of events is defined; the hart is inert */ + def grounded: Receive = commonBehavior + .orElse { + case HartTimer.PairWith(_, pad, shuttle, from) => + pairWith(pad, shuttle, from) + + case _ => ; + } + + /** behaviors that are valid after a sequence of events is defined; the hart is active */ + def flightsScheduled: Receive = commonBehavior + .orElse { + case HartTimer.PairWith(_, pad, shuttle, from) => + pairWith(pad, shuttle, from) + val event = sequence(sequenceIndex) + if (event.lockedDoors) { + from ! HartTimer.LockDoors + } + if (event.docked.contains(true)) { + from ! HartTimer.ShuttleDocked(zoneId) + } + + case HartTimer.NextEvent(next) if next == 0 => + sequence = delayedScheduleChange.getOrElse(sequence) + sequenceLength = sequence.length + delayedScheduleChange = None + nextEvent(next) + + case HartTimer.NextEvent(next) => + nextEvent(next) + + case HartTimer.Update(_, forChannel) => + val seq = sequence + val event = seq(sequenceIndex) + val time = Some(System.currentTimeMillis() - lastStartTime) + if (event.docked.contains(true)) { + padEvents.publish( HartTimer.ShuttleDocked(forChannel) ) + } + event.prerequisiteUpdate match { + case Some(fields) => + val times = event.timeFields(time) + zone.LocalEvents ! LocalServiceMessage( + forChannel, + LocalAction.ShuttleEvent(HartTimer.OrbitalShuttleEvent( + fields.u1, fields.u2, times.t1, times.t2, times.t3, padAndShuttlePairs zip Seq(20, 20, 20) + )) + ) + case None => ; + } + zone.LocalEvents ! LocalServiceMessage( + forChannel, + LocalAction.ShuttleEvent( + HartTimer.analyzeEvent(event, padAndShuttlePairs, time) + ) + ) + event.shuttleState match { + case Some(state) => + padEvents.publish( HartTimer.ShuttleStateUpdate(forChannel, state.id) ) + case None => + //find previous valid shuttle state + var i = sequenceIndex - 1 + while(seq(i).shuttleState.isEmpty) { i = if (i - 1 < 0) sequenceLength - 1 else i - 1 } + padEvents.publish( HartTimer.ShuttleStateUpdate(forChannel, seq(i).shuttleState.get.id) ) + } + + case _ => ; + } + + def receive: Receive = grounded + + def pairWith(pad: PlanetSideGUID, shuttle: PlanetSideGUID, from: ActorRef): Unit = { + padEvents.subscribe(from, to = "") + padAndShuttlePairs = (padAndShuttlePairs :+ (pad, shuttle)).distinct + } + + def nextEvent(next: Int): Unit = { + val currEvent = sequence(sequenceIndex) + val event = sequence(next) + sequenceIndex = next + lastStartTime = System.currentTimeMillis() + timer = context.system.scheduler.scheduleOnce( + event.duration milliseconds, + self, + HartTimer.NextEvent((next + 1) % sequenceLength) + ) + //updates + val evt = HartTimer.analyzeEvent(event, padAndShuttlePairs) + event.docked match { + case Some(true) if currEvent.docked.isEmpty => + zone.LocalEvents ! LocalServiceMessage(zoneId, LocalAction.ShuttleEvent(evt)) + padEvents.publish( shuttleDockedInThisZone ) + case Some(false) if currEvent.docked.contains(true) => + padEvents.publish( shuttleFreeFromDockInThisZone ) + context.system.scheduler.scheduleOnce( + delay = 10 milliseconds, + zone.LocalEvents, + LocalServiceMessage(zoneId, LocalAction.ShuttleEvent(evt)) + ) + case _ => + zone.LocalEvents ! LocalServiceMessage(zoneId, LocalAction.ShuttleEvent(evt)) + } + if (currEvent.lockedDoors != event.lockedDoors) { + padEvents.publish( if(event.lockedDoors) HartTimer.LockDoors else HartTimer.UnlockDoors ) + } + event.shuttleState match { + case Some(state) => + padEvents.publish( HartTimer.ShuttleStateUpdate(zoneId, state.id) ) + case None => ; + } + } +} + +object HartTimer { + /** + * Transform `HartEvent` data into `OrbitalShuttleEvent` data. + * The former is treated as something internal. + * The latter is treated as something external. + * @see `OrbitalShuttleEvent` + * @see `HartEvent` + * @param event the `TimeShuttleEvent` data + * @param time how long has the current event in th sequence been occurring + * @return the `OrbitalShuttleEvent` data + */ + def analyzeEvent( + event: HartEvent, + padAndShuttlePairs: List[(PlanetSideGUID, PlanetSideGUID)], + time: Option[Long] = None + ): OrbitalShuttleEvent = { + import net.psforever.services.hart.HartEvent._ + val stateFields = event.stateFields(time) + val timeFields = event.timeFields(time) + //these control codes are taken from packets samples for VS sanctuary during a specific few sequences + //while the number varies - from 5 to 37 and an actual maximum of 63 - their purpose seems indeterminate + val pairs = event match { + case _: Boarding => Seq(20, 20, 20) + case _: ShuttleTakeoffOps => Seq(20, 20, 20) + case _: Takeoff => Seq( 6, 25, 5) + case _: InTransit => Seq(20, 20, 20) + case Arrival => Seq( 5, 5, 27) + case ShuttleDockingOps => Seq(20, 20, 20) + case Blanking => Seq(20, 20, 20) + case _ => Seq(20, 20, 20) + } + OrbitalShuttleEvent( + stateFields.u1, stateFields.u2, + timeFields.t1, timeFields.t2, timeFields.t3, + padAndShuttlePairs zip pairs + ) + } + + /** + * Internal message to advance the sequence event. + * @param index the position of the next event + */ + private case class NextEvent(index: Int) + + trait MessageToHartInZone { + def inZone: String + } + + /** + * Personalized messages that align the state of the shuttle to one's perspective (client). + * @param inZone the zone for which the update will be composed + * @param forChannel to whom to address the reply + */ + final case class Update(inZone: String, forChannel: String) extends MessageToHartInZone + + final case class SetEventDurations(inZone: String, away: Long, boarding: Long) extends MessageToHartInZone + /** + * Append information about a building amenity and shuttle combination in this zone. + * @param zone the relevant zone + * @param pad the orbital shuttle pad (`obbasemesh`) + * @param shuttle the orbital shuttle + * @param from the control agency of the pad + */ + final case class PairWith(zone: Zone, pad: PlanetSideGUID, shuttle: PlanetSideGUID, from: ActorRef) + /** + * Data structure for passing information about the event to client-local space. + * The fields match the `OrbitalShuttleTimeMsg` packet that is created using this data. + * @see `OrbitalShuttleTimeMsg` + */ + final case class OrbitalShuttleEvent( + u1: HartSequence, + u2: Int, + t1: Long, + t2: Long, + t3: Long, + pairs: List[((PlanetSideGUID, PlanetSideGUID), Int)] + ) + + /** + * Design for the envelop for the message bus + * to relay instructions back to the individual facility amenity portions of this HART system. + * The channel is blank because it does not need special designation. + */ + trait Command extends GenericEventBusMsg { def channel: String = "" } + /** + * Forbid entry through the boartding gantry doors. + */ + case object LockDoors extends Command + /** + * Permit entry through the boartding gantry doors. + */ + case object UnlockDoors extends Command + /** + * The state exists to be turned into, ultimately, a `VehicleStateMessage` packet for the shuttle. + * This state is to be loaded into the `flying` field. + * @see `VehicleStateMessage` + * @param state shuttle state, probably more symbolic of a gvien state than anything else + */ + final case class ShuttleStateUpdate(forChannel: String, state: Int) extends Command + /** + * The shuttle has landed on the pad and will (soon) accept passengers. + */ + final case class ShuttleDocked(forChannel: String) extends Command + /** + * The shuttle has disengaged from the pad, will no longer accept passengers, and may take off soon. + */ + final case class ShuttleFreeFromDock(forChannel: String) extends Command +} diff --git a/src/main/scala/net/psforever/services/hart/HartTimerActions.scala b/src/main/scala/net/psforever/services/hart/HartTimerActions.scala new file mode 100644 index 00000000..5497e64f --- /dev/null +++ b/src/main/scala/net/psforever/services/hart/HartTimerActions.scala @@ -0,0 +1,59 @@ +// Copyright (c) 2021 PSForever +package net.psforever.services.hart + +import net.psforever.objects.Vehicle +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad +import net.psforever.services.local.{LocalAction, LocalServiceMessage} + +object HartTimerActions { + /** + * Update the shuttle's mounted arrangement with the pad, setting the state. + * @param pad the orbital shuttle pad + * @param shuttle the orbital shuttle pad's shuttle + * @param toChannel to whom these messages will be dispatched + */ + def ShuttleDocked(pad: OrbitalShuttlePad, shuttle: Vehicle, toChannel: String): Unit = { + val zone = pad.Zone + if(toChannel.equals(zone.id)) { + shuttle.MountedIn = pad.GUID + } + zone.LocalEvents ! LocalServiceMessage( + toChannel, + LocalAction.ShuttleDock(pad.GUID, shuttle.GUID, 3) + ) + } + + /** + * Update the shuttle's mounted arrangement with the pad, undoing any connection. + * @param pad the orbital shuttle pad + * @param shuttle the orbital shuttle pad's shuttle + * @param toChannel to whom these messages will be dispatched + */ + def ShuttleFreeFromDock(pad: OrbitalShuttlePad, shuttle: Vehicle, toChannel: String): Unit = { + val zone = pad.Zone + if(toChannel.equals(zone.id)) { + shuttle.MountedIn = None + } + zone.LocalEvents ! LocalServiceMessage( + toChannel, + LocalAction.ShuttleUndock(pad.GUID, shuttle.GUID, shuttle.Position, shuttle.Orientation) + ) + } + + /** + * Update the shuttle's flight state. + * @param pad the orbital shuttle pad + * @param shuttle the orbital shuttle pad's shuttle + * @param toChannel to whom these messages will be dispatched + */ + def ShuttleStateUpdate(pad: OrbitalShuttlePad, shuttle: Vehicle, toChannel: String, state: Int): Unit = { + val zone = pad.Zone + if(toChannel.equals(zone.id)) { + shuttle.Flying = state + } + zone.LocalEvents ! LocalServiceMessage( + toChannel, + LocalAction.ShuttleState(shuttle.GUID, shuttle.Position, shuttle.Orientation, state) + ) + } +} \ No newline at end of file diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 3348f6f0..23115762 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -27,27 +27,19 @@ class LocalService(zone: Zone) extends Actor { context.actorOf(Props[RouterTelepadActivation](), s"${zone.id}-telepad-activate-agent") private[this] val log = org.log4s.getLogger - override def preStart() = { - log.trace(s"Awaiting ${zone.id} local events ...") - } - val LocalEvents = new GenericEventBus[LocalServiceResponse] def receive: Receive = { case Service.Join(channel) => val path = s"/$channel/Local" - val who = sender() - log.info(s"$who has joined $path") - LocalEvents.subscribe(who, path) + LocalEvents.subscribe(sender(), path) case Service.Leave(None) => LocalEvents.unsubscribe(sender()) case Service.Leave(Some(channel)) => val path = s"/$channel/Local" - val who = sender() - log.info(s"$who has left $path") - LocalEvents.unsubscribe(who, path) + LocalEvents.unsubscribe(sender(), path) case Service.LeaveAll() => LocalEvents.unsubscribe(sender()) @@ -83,6 +75,12 @@ class LocalService(zone: Zone) extends Actor { LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.DoorCloses(door_guid)) ) + case LocalAction.DoorSlamsShut(door) => + val door_guid = door.GUID + doorCloser ! SupportActor.HurrySpecific(List(door), zone) + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", Service.defaultPlayerGUID, LocalResponse.DoorCloses(door_guid)) + ) case LocalAction.HackClear(player_guid, target, unk1, unk2) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.SendHackMessageHackCleared(target.GUID, unk1, unk2)) @@ -175,6 +173,14 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.RouterTelepadTransport(passenger_guid, src_guid, dest_guid) ) ) + case LocalAction.SendResponse(pkt) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.SendResponse(pkt) + ) + ) case LocalAction.SetEmpire(object_guid, empire) => LocalEvents.publish( LocalServiceResponse( @@ -183,6 +189,34 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.SetEmpire(object_guid, empire) ) ) + case LocalAction.ShuttleDock(pad, shuttle, slot) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ShuttleDock(pad, shuttle, slot) + ) + ) + case LocalAction.ShuttleUndock(pad, shuttle, pos, orient) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ShuttleUndock(pad, shuttle, pos, orient) + ) + ) + case LocalAction.ShuttleEvent(ev) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", Service.defaultPlayerGUID, LocalResponse.ShuttleEvent(ev)) + ) + case LocalAction.ShuttleState(guid, pos, orient, state) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ShuttleState(guid, pos, orient, state) + ) + ) case LocalAction.ToggleTeleportSystem(player_guid, router, system_plan) => LocalEvents.publish( LocalServiceResponse( @@ -219,7 +253,7 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.TriggerSound(sound, pos, unk, volume) ) ) - case LocalAction.UpdateForceDomeStatus(player_guid, building_guid, activated) => { + case LocalAction.UpdateForceDomeStatus(player_guid, building_guid, activated) => LocalEvents.publish( LocalServiceResponse( s"/$forChannel/Local", @@ -227,7 +261,6 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.UpdateForceDomeStatus(building_guid, activated) ) ) - } case LocalAction.RechargeVehicleWeapon(player_guid, vehicle_guid, weapon_guid) => LocalEvents.publish( LocalServiceResponse( @@ -248,13 +281,6 @@ class LocalService(zone: Zone) extends Actor { //response from HackClearActor case HackClearActor.SendHackMessageHackCleared(target_guid, _, unk1, unk2) => log.info(s"Clearing hack for $target_guid") - LocalEvents.publish( - LocalServiceResponse( - s"/${zone.id}/Local", - Service.defaultPlayerGUID, - LocalResponse.SendHackMessageHackCleared(target_guid, unk1, unk2) - ) - ) //message from ProximityTerminalControl case Terminal.StartProximityEffect(terminal) => @@ -284,9 +310,9 @@ class LocalService(zone: Zone) extends Actor { if (seats.count(_.isOccupied) > 0) { val wasKickedByDriver = false //TODO yeah, I don't know seats.foreach(seat => { - seat.Occupant match { + seat.occupant match { case Some(tplayer) => - seat.Occupant = None + seat.unmount(tplayer) tplayer.VehicleSeated = None zone.VehicleEvents ! VehicleServiceMessage( zone.id, @@ -342,7 +368,7 @@ class LocalService(zone: Zone) extends Actor { //get rid of previous linked remote telepad (if any) zone.GUID(internalTelepad.Telepad) match { case Some(old: TelepadDeployable) => - log.info( + log.trace( s"ActivateTeleportSystem: old remote telepad@${old.GUID.guid} linked to internal@${internalTelepad.GUID.guid} will be deconstructed" ) old.Active = false @@ -352,7 +378,7 @@ class LocalService(zone: Zone) extends Actor { } internalTelepad.Telepad = remoteTelepad.GUID if (internalTelepad.Active) { - log.info( + log.trace( s"ActivateTeleportSystem: fully deployed router@${router.GUID.guid} in ${zone.id} will link internal@${internalTelepad.GUID.guid} and remote@${remoteTelepad.GUID.guid}" ) LocalEvents.publish( diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index 43fe2cf2..0eb988fb 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -15,6 +15,7 @@ import net.psforever.packet.game.GenericActionEnum.GenericActionEnum import net.psforever.packet.game.GenericObjectActionEnum.GenericObjectActionEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.game.{ChatMsg, DeployableInfo, DeploymentAction, TriggeredSound} +import net.psforever.services.hart.HartTimer.OrbitalShuttleEvent import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} final case class LocalServiceMessage(forChannel: String, actionMessage: LocalAction.Action) @@ -38,6 +39,7 @@ object LocalAction { final case class Detonate(guid: PlanetSideGUID, obj: PlanetSideGameObject) extends Action final case class DoorOpens(player_guid: PlanetSideGUID, continent: Zone, door: Door) extends Action final case class DoorCloses(player_guid: PlanetSideGUID, door_guid: PlanetSideGUID) extends Action + final case class DoorSlamsShut(door: Door) extends Action final case class HackClear(player_guid: PlanetSideGUID, target: PlanetSideServerObject, unk1: Long, unk2: Long = 8L) extends Action final case class HackTemporarily( @@ -87,7 +89,16 @@ object LocalAction { src_guid: PlanetSideGUID, dest_guid: PlanetSideGUID ) extends Action + final case class SendResponse(pkt: PlanetSideGamePacket) extends Action final case class SetEmpire(object_guid: PlanetSideGUID, empire: PlanetSideEmpire.Value) extends Action + final case class ShuttleDock(pad_guid: PlanetSideGUID, shuttle_guid: PlanetSideGUID, toSlot: Int) extends Action + final case class ShuttleUndock( + pad_guid: PlanetSideGUID, + shuttle_guid: PlanetSideGUID, + pos: Vector3, orient: Vector3 + ) extends Action + final case class ShuttleEvent(ev: OrbitalShuttleEvent) extends Action + final case class ShuttleState(guid: PlanetSideGUID, pos: Vector3, orientation: Vector3, state: Int) extends Action final case class ToggleTeleportSystem( player_guid: PlanetSideGUID, router: Vehicle, diff --git a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala index 123fe140..48626712 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala @@ -11,9 +11,11 @@ import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.GenericActionEnum.GenericActionEnum import net.psforever.packet.game.GenericObjectActionEnum.GenericObjectActionEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum +import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game._ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.services.GenericEventBusMsg +import net.psforever.services.hart.HartTimer.OrbitalShuttleEvent final case class LocalServiceResponse( channel: String, @@ -58,7 +60,16 @@ object LocalResponse { src_guid: PlanetSideGUID, dest_guid: PlanetSideGUID ) extends Response + final case class SendResponse(pkt: PlanetSideGamePacket) extends Response final case class SetEmpire(object_guid: PlanetSideGUID, empire: PlanetSideEmpire.Value) extends Response + final case class ShuttleDock(pad_guid: PlanetSideGUID, shuttle_guid: PlanetSideGUID, toSlot: Int) extends Response + final case class ShuttleUndock( + pad_guid: PlanetSideGUID, + shuttle_guid: PlanetSideGUID, + pos: Vector3, orient: Vector3 + ) extends Response + final case class ShuttleEvent(ev: OrbitalShuttleEvent) extends Response + final case class ShuttleState(guid: PlanetSideGUID, pos: Vector3, orientation: Vector3, state: Int) extends Response final case class ToggleTeleportSystem( router: Vehicle, systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)] diff --git a/src/main/scala/net/psforever/services/local/support/DeployableRemover.scala b/src/main/scala/net/psforever/services/local/support/DeployableRemover.scala index c7aeef7f..0d8f1e65 100644 --- a/src/main/scala/net/psforever/services/local/support/DeployableRemover.scala +++ b/src/main/scala/net/psforever/services/local/support/DeployableRemover.scala @@ -50,7 +50,7 @@ class DeployableRemover(taskResolver: ActorRef) extends RemoverActor(taskResolve override def SecondJob(entry: RemoverActor.Entry): Unit = { val obj = entry.obj.asInstanceOf[PlanetSideGameObject with Deployable] - info(s"Deleting a ${obj.Definition.Name} deployable") + trace(s"Deleting a ${obj.Definition.Name} deployable") context.parent ! DeployableRemover.EliminateDeployable(obj, obj.GUID, obj.Position, entry.zone) super.SecondJob(entry) } diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index c829871d..477f7f1c 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -30,7 +30,7 @@ class HackCaptureActor(val taskResolver: ActorRef) extends Actor { def receive: Receive = { case HackCaptureActor.StartCaptureTerminalHack(target, zone, unk1, unk2, startTime) => - log.trace(s"${target.GUID} is hacked.") + log.trace(s"StartCaptureTerminalHack: ${target.GUID} is hacked.") val duration = target.Definition match { case GlobalDefinitions.capture_terminal => @@ -54,7 +54,7 @@ class HackCaptureActor(val taskResolver: ActorRef) extends Actor { hackedObjects.find(_.target == target) match { case Some(_) => log.trace( - s"${target.GUID} was already hacked - removing it from the hacked objects list before re-adding it." + s"StartCaptureTerminalHack: ${target.GUID} was already hacked - removing it from the hacked objects list before re-adding it." ) hackedObjects = hackedObjects.filterNot(x => x.target == target) case _ => ; @@ -76,7 +76,7 @@ class HackCaptureActor(val taskResolver: ActorRef) extends Actor { val finishedHacks = hackedObjects.filter(x => now - x.hack_timestamp >= x.duration.toNanos) hackedObjects = stillHacked finishedHacks.foreach(entry => { - log.trace(s"Capture terminal hack timeout reached for terminal ${entry.target.GUID}") + log.trace(s"ProcessCompleteHacks: capture terminal hack timeout reached for terminal ${entry.target.GUID}") val hackedByFaction = entry.target.HackedBy.get.hackerFaction entry.target.Actor ! CommonMessages.ClearHack() @@ -211,7 +211,7 @@ class HackCaptureActor(val taskResolver: ActorRef) extends Actor { val short_timeout: FiniteDuration = math.max(1, hackEntry.duration.toNanos - (System.nanoTime - hackEntry.hack_timestamp)) nanoseconds - log.trace(s"Still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds") + log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds") import scala.concurrent.ExecutionContext.Implicits.global clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks()) } diff --git a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala index 3f2fe614..0fcd4fed 100644 --- a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala @@ -83,12 +83,12 @@ class HackClearActor() extends Actor { case Some(hackEntry) => val short_timeout: FiniteDuration = math.max(1, hackEntry.duration - (now - hackEntry.time)) nanoseconds - log.info( - s"HackClearActor: Still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds" + log.debug( + s"HackClearActor: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds" ) import scala.concurrent.ExecutionContext.Implicits.global clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackClearActor.TryClearHacks()) - case None => log.info("HackClearActor: No objects left in hacked objects list. Not rescheduling check.") + case None => log.debug("HackClearActor: no objects left in hacked objects list. Not rescheduling check.") } } diff --git a/src/main/scala/net/psforever/services/properties/PropertyOverrideManager.scala b/src/main/scala/net/psforever/services/properties/PropertyOverrideManager.scala index f296341c..967828f4 100644 --- a/src/main/scala/net/psforever/services/properties/PropertyOverrideManager.scala +++ b/src/main/scala/net/psforever/services/properties/PropertyOverrideManager.scala @@ -31,16 +31,16 @@ class PropertyOverrideManager extends Actor { } private def LoadOverridesFromFile(zoneId: Int): Unit = { - val zoneOverrides = LoadFile(s"overrides/game_objects${zoneId}.adb.lst") + val zoneOverrides = LoadFile(s"overrides/game_objects$zoneId.adb.lst") if (zoneOverrides == null) { - log.debug(s"No overrides found for zone ${zoneId} using filename game_objects${zoneId}.adb.lst") + log.debug(s"PropertyOverride: no overrides found for zone $zoneId using filename game_objects$zoneId.adb.lst") return } val grouped = zoneOverrides.groupBy(_._1).view.mapValues(_.map(x => (x._2, x._3)).toList).toMap - log.debug(s"Loaded property overrides for zone $zoneId: ${grouped.toString}") + log.debug(s"PropertyOverride: loaded property overrides for zone $zoneId: ${grouped.toString}") overrides += (zoneId -> grouped) } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index d59bcf49..ab90f163 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -124,11 +124,7 @@ class SquadService extends Actor { private[this] val log = org.log4s.getLogger private def debug(msg: String): Unit = { - log.info(msg) - } - - override def preStart(): Unit = { - log.info("Starting...") + log.debug(msg) } override def postStop(): Unit = { @@ -321,7 +317,7 @@ class SquadService extends Actor { case str if str.matches("//d+") => Publish(to.toLong, msg, excluded) case _ => - log.error(s"Publish(String): subscriber information is an unhandled format - $to") + log.warn(s"Publish(String): subscriber information is an unhandled format - $to") } } @@ -336,7 +332,7 @@ class SquadService extends Actor { case Some(user) => user ! SquadServiceResponse("", msg) case None => - log.error(s"Publish(Long): subscriber information can not be found - $to") + log.warn(s"Publish(Long): subscriber information can not be found - $to") } } @@ -383,7 +379,6 @@ class SquadService extends Actor { case Service.Join(faction) if "TRNCVS".indexOf(faction) > -1 => val path = s"/$faction/Squad" val who = sender() - debug(s"$who has joined $path") SquadEvents.subscribe(who, path) //subscribe to the player's personal channel - necessary for future and previous squad information @@ -392,7 +387,6 @@ class SquadService extends Actor { val longCharId = char_id.toLong val path = s"/$char_id/Squad" val who = sender() - debug(s"$who has joined $path") context.watch(who) UserEvents += longCharId -> who refused(longCharId) = Nil @@ -407,7 +401,6 @@ class SquadService extends Actor { case Service.Leave(Some(faction)) if "TRNCVS".indexOf(faction) > -1 => val path = s"/$faction/Squad" val who = sender() - debug(s"$who has left $path") SquadEvents.unsubscribe(who, path) case Service.Leave(Some(char_id)) => diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala index dab15d0b..5edb6c04 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala @@ -18,27 +18,19 @@ class VehicleService(zone: Zone) extends Actor { private val turretUpgrade: ActorRef = context.actorOf(Props[TurretUpgrader](), s"${zone.id}-turret-upgrade-agent") private[this] val log = org.log4s.getLogger - override def preStart() = { - log.trace(s"Awaiting ${zone.id} vehicle events ...") - } - val VehicleEvents = new GenericEventBus[VehicleServiceResponse] def receive = { case Service.Join(channel) => val path = s"/$channel/Vehicle" - val who = sender() - log.info(s"$who has joined $path") - VehicleEvents.subscribe(who, path) + VehicleEvents.subscribe(sender(), path) case Service.Leave(None) => VehicleEvents.unsubscribe(sender()) case Service.Leave(Some(channel)) => val path = s"/$channel/Vehicle" - val who = sender() - log.info(s"$who has left $path") - VehicleEvents.unsubscribe(who, path) + VehicleEvents.unsubscribe(sender(), path) case Service.LeaveAll() => VehicleEvents.unsubscribe(sender()) @@ -105,6 +97,14 @@ class VehicleService(zone: Zone) extends Actor { VehicleResponse.KickPassenger(seat_num, kickedByDriver, vehicle_guid) ) ) + case VehicleAction.ObjectDelete(guid) => + VehicleEvents.publish( + VehicleServiceResponse( + s"/$forChannel/Vehicle", + Service.defaultPlayerGUID, + VehicleResponse.ObjectDelete(guid) + ) + ) case VehicleAction.LoadVehicle(player_guid, vehicle, vtype, vguid, vdata) => VehicleEvents.publish( VehicleServiceResponse( @@ -379,7 +379,7 @@ class VehicleService(zone: Zone) extends Actor { } case msg => - log.info(s"Unhandled message $msg from ${sender()}") + log.warn(s"Unhandled message $msg from ${sender()}") } import net.psforever.objects.serverobject.tube.SpawnTube diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala index 147d597c..8f2b5d7b 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala @@ -63,7 +63,7 @@ object VehicleAction { vdata: ConstructorData ) extends Action final case class MountVehicle(player_guid: PlanetSideGUID, object_guid: PlanetSideGUID, seat: Int) extends Action - final case class ObjectDelete(player_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID) extends Action + final case class ObjectDelete(guid: PlanetSideGUID) extends Action final case class Ownership(player_guid: PlanetSideGUID, vehicle_guid: PlanetSideGUID) extends Action final case class PlanetsideAttribute( player_guid: PlanetSideGUID, diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala index aa9c2ed3..87b724e9 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala @@ -45,11 +45,12 @@ object VehicleResponse { final case class LoadVehicle(vehicle: Vehicle, vtype: Int, vguid: PlanetSideGUID, vdata: ConstructorData) extends Response final case class MountVehicle(object_guid: PlanetSideGUID, seat: Int) extends Response + final case class ObjectDelete(guid: PlanetSideGUID) extends Response final case class Ownership(vehicle_guid: PlanetSideGUID) extends Response final case class PlanetsideAttribute(vehicle_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) extends Response - final case class RevealPlayer(player_guid: PlanetSideGUID) extends Response - final case class SeatPermissions(vehicle_guid: PlanetSideGUID, seat_group: Int, permission: Long) extends Response + final case class RevealPlayer(player_guid: PlanetSideGUID) extends Response + final case class SeatPermissions(vehicle_guid: PlanetSideGUID, seat_group: Int, permission: Long) extends Response final case class StowEquipment( vehicle_guid: PlanetSideGUID, slot: Int, diff --git a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala index 0278da41..d3709e71 100644 --- a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala +++ b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala @@ -154,8 +154,8 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { target.Seats.values .filter { _.isOccupied } .foreach({ seat => - val tplayer = seat.Occupant.get - seat.Occupant = None + val tplayer = seat.occupant.get + seat.unmount(tplayer) tplayer.VehicleSeated = None if (tplayer.HasGUID) { context.parent ! VehicleServiceMessage( @@ -164,7 +164,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { ) } }) - info(s"Converting manned wall turret weapon to $upgrade") + debug(s"Converting manned wall turret weapon to $upgrade") val oldBoxes = AllMountedWeaponMagazines(target) target.Upgrade = upgrade //perform upgrade @@ -228,7 +228,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { def FinishUpgradingTurret(entry: TurretUpgrader.Entry)(): Unit = { val target = entry.obj.asInstanceOf[FacilityTurret] val zone = entry.zone - info(s"Wall turret finished ${target.Upgrade} upgrade") + trace(s"Wall turret finished ${target.Upgrade} upgrade") target.ConfirmUpgrade(entry.upgrade) val targetGUID = target.GUID if (target.Health > 0) { diff --git a/src/main/scala/net/psforever/types/CharacterGender.scala b/src/main/scala/net/psforever/types/CharacterGender.scala deleted file mode 100644 index 8f05bca9..00000000 --- a/src/main/scala/net/psforever/types/CharacterGender.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.types - -import net.psforever.packet.PacketHelpers -import scodec.codecs.uint2L - -/** - * Values for two genders, Male and Female, starting at 1 = Male. - */ -object CharacterGender extends Enumeration(1) { - type Type = Value - - val Male, Female = Value - - implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L) -} diff --git a/src/main/scala/net/psforever/types/CharacterSex.scala b/src/main/scala/net/psforever/types/CharacterSex.scala new file mode 100644 index 00000000..28b8d237 --- /dev/null +++ b/src/main/scala/net/psforever/types/CharacterSex.scala @@ -0,0 +1,45 @@ +// Copyright (c) 2021 PSForever +package net.psforever.types + +import enumeratum.values.{IntEnum, IntEnumEntry} +import net.psforever.packet.PacketHelpers +import scodec.codecs.uint2L + +/** + * Values for two sexes, Male and Female, as required by `ObjectCreateMessage` parameters. + * Some quaint language for log decoration is provided. + * Blame the lack of gender dysphoria on the Terran Republic. + */ +sealed abstract class CharacterSex( + val value: Int, + val pronounSubject: String, + val pronounObject: String, + val possessive: String + ) extends IntEnumEntry { + def possessiveNoObject: String = possessive +} + +/** + * Values for two sexes, Male and Female. + */ +object CharacterSex extends IntEnum[CharacterSex] { + val values = findValues + + case object Male extends CharacterSex( + value = 1, + pronounSubject = "he", + pronounObject = "him", + possessive = "his" + ) + + case object Female extends CharacterSex( + value = 2, + pronounSubject = "she", + pronounObject = "her", + possessive = "her" + ) { + override def possessiveNoObject: String = "hers" + } + + implicit val codec = PacketHelpers.createIntEnumCodec(enum = this, uint2L) +} diff --git a/src/main/scala/net/psforever/types/CharacterVoice.scala b/src/main/scala/net/psforever/types/CharacterVoice.scala index 007b96c4..a6e287d4 100644 --- a/src/main/scala/net/psforever/types/CharacterVoice.scala +++ b/src/main/scala/net/psforever/types/CharacterVoice.scala @@ -10,7 +10,7 @@ import scodec.codecs.uint * While it is technically not valid to have a wrong-gendered voice, * unlisted sixth and seventh entries would give a male character a female voice; * a female character with either entry would become mute, however. - * @see `CharacterGender` + * @see `CharacterSex` */ object CharacterVoice extends Enumeration { type Type = Value diff --git a/src/main/scala/net/psforever/types/HartSequence.scala b/src/main/scala/net/psforever/types/HartSequence.scala new file mode 100644 index 00000000..01b4fb95 --- /dev/null +++ b/src/main/scala/net/psforever/types/HartSequence.scala @@ -0,0 +1,26 @@ +package net.psforever.types + +import enumeratum.values.{IntEnum, IntEnumEntry} + +sealed abstract class HartSequence(val value: Int) extends IntEnumEntry + +object HartSequence extends IntEnum[HartSequence] { + val values = findValues + + /** no effect, but is used when the shuttle is docked */ + case object State0 extends HartSequence(value = 0) + /** gantries retract, lights on, bay doors open, platform up */ + case object PrepareForDeparture extends HartSequence(value = 1) + /** shuttle takes off, bay doors close, lights off */ + case object TakeOff extends HartSequence(value = 2) + /** lights on, bay doors open, shuttle lands */ + case object Land extends HartSequence(value = 3) + /** platform down, bay doors closed, gantries extend, lights off */ + case object PrepareForBoarding extends HartSequence(value = 4) + /** no effect, but is used when the shuttle is away; + * a substitute for 7 occasionally or used as its supplement, e.g., 2-2, 5-3, 3-4 OR 2-2, 7-3, 5-3, 3-4 + */ + case object State5 extends HartSequence(value = 5) + /** no effect, but is used when the shuttle is away */ + case object State7 extends HartSequence(value = 7) +} diff --git a/src/main/scala/net/psforever/types/Vector3.scala b/src/main/scala/net/psforever/types/Vector3.scala index 43c4f43f..ea821ce0 100644 --- a/src/main/scala/net/psforever/types/Vector3.scala +++ b/src/main/scala/net/psforever/types/Vector3.scala @@ -117,6 +117,14 @@ object Vector3 { */ def z(value: Float): Vector3 = Vector3(0, 0, value) + /** + * Calculate the negation of this vector, + * the same vector in the antiparallel direction. + * @param v the original vector + * @return the negation of the original vector + */ + def neg(v: Vector3): Vector3 = Vector3(-v.x, -v.y, -v.z) + /** * Calculate the actual distance between two points. * @param pos1 the first point @@ -323,8 +331,8 @@ object Vector3 { val sin = math.sin(ang).toFloat val (x, y, z) = (vec.x, vec.y, vec.z) Vector3( - closeToInsignificance(x * cos - y * sin), - closeToInsignificance(x * sin - y * cos), + closeToInsignificance(x * cos + y * sin), + closeToInsignificance(y * cos - x * sin), z ) } @@ -379,9 +387,9 @@ object Vector3 { /** * Given a `Vector3` element composed of Euler angles - * and a `Vector3` element in the direction of "up", - * find a standard unit vector that points in the direction of "up" after rotating by the Euler angles. - * Compass direction rules apply (North is 0 degrees, East is 90 degrees, etc.). + * and a `Vector3` element in the vector direction of "up", + * find a standard unit vector that points in the direction of the entity's "up" after rotating by the Euler angles. + * Compass direction rules apply for the z-axis (North is 0 degrees, East is 90 degrees, etc.). * @see `Vector3.Rx(Float)` * @see `Vector3.Ry(Float)` * @see `Vector3.Rz(Float)` @@ -390,7 +398,12 @@ object Vector3 { * @return a mathematical vector representing a relative "up" direction */ def relativeUp(orient: Vector3, up: Vector3): Vector3 = { - //TODO is the missing calculation before Rz(Rx(Ry(v, x), y), z) or after Rz(Ry(Rx(v, y), x), z)? - Rz(Rx(up, orient.y), (orient.z + 180) % 360f) + /* + rotate in Ry using the x-component and rotate in Rx using the y-component + only Rz is rotated using its corresponding component, and you add 180 clamping to 0-360 degrees + I'm sure mathematicians know what's going on here, but I don't + the purpose of this comment is to make certain that the future me knows that all this is not a mistake + */ + Rz(Rx(Ry(Unit(up), orient.x), -orient.y), (orient.z + 180) % 360f) } } diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index f85e30ff..e80a5bbf 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -136,7 +136,8 @@ case class GameConfig( amenityAutorepairDrainRate: Float, bepRate: Double, cepRate: Double, - newAvatar: NewAvatar + newAvatar: NewAvatar, + hart: HartConfig ) case class NewAvatar( @@ -144,6 +145,11 @@ case class NewAvatar( cr: CommandRank ) +case class HartConfig( + inFlightDuration: Long, + boardingDuration: Long +) + case class DevelopmentConfig( unprivilegedGmCommands: Seq[ChatMessageType], netSim: NetSimConfig diff --git a/src/main/scala/net/psforever/util/PointOfInterest.scala b/src/main/scala/net/psforever/util/PointOfInterest.scala index e6e70866..4c04f4d9 100644 --- a/src/main/scala/net/psforever/util/PointOfInterest.scala +++ b/src/main/scala/net/psforever/util/PointOfInterest.scala @@ -187,7 +187,7 @@ object PointOfInterest { * @return all of the location keys */ def listLocations(zone: PointOfInterest): String = { - var out: String = "warps: " + var out: String = "Locations: " if (zone.locations.nonEmpty) { out += zone.locations.keys.toArray.sorted.mkString(", ") } else @@ -201,7 +201,7 @@ object PointOfInterest { * @return all of the warpgate keys */ def listWarpgates(zone: PointOfInterest): String = { - var out: String = "gatenames: " + var out: String = "Gates: " if (zone.gates.isEmpty) out += "none" else @@ -209,6 +209,15 @@ object PointOfInterest { out } + /** + * Get the name for all of the warpgates and locations that can be visited in this `CSRZone`. + * @param zone the `CSRZone` + * @return all of the warpgate and location keys + */ + def listAll(zone: PointOfInterest): String = { + s"${listWarpgates(zone)} ${listLocations(zone)}" + } + /** * Select, of all the `CSRZone` locations and warpgates, a pseudorandom destination to spawn the player in the zone if none has been specified. * @param zone the `CSRZone` @@ -364,7 +373,7 @@ object PointOfInterest { "anguta" -> Vector3(3999, 4170, 266), "igaluk" -> Vector3(3241, 5658, 235), "keelut" -> Vector3(3630, 1904, 265), - "nerrivik" -> Vector3(3522, 3703, 322), + "nerrivik" -> Vector3(3522, 3703, 222), "pinga" -> Vector3(5938, 3545, 96), "sedna" -> Vector3(3932, 5160, 232), "tarqaq" -> Vector3(2980, 2155, 237), diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index 18269b6e..e9863e96 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -18,6 +18,7 @@ import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.pad.{VehicleSpawnPad, VehicleSpawnPadDefinition} import net.psforever.objects.serverobject.painbox.{Painbox, PainboxDefinition} import net.psforever.objects.serverobject.resourcesilo.ResourceSilo +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.serverobject.structures.{Building, BuildingDefinition, FoundationBuilder, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalDefinition} import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech @@ -348,6 +349,14 @@ object Zones { owningBuildingGuid = ownerGuid ) + case "gr_door_mb_orb" => + zoneMap + .addLocalObject( + obj.guid, + Door.Constructor(obj.position, GlobalDefinitions.gr_door_mb_orb), + owningBuildingGuid = ownerGuid + ) + case objectType if doorTypes.contains(objectType) => zoneMap .addLocalObject(obj.guid, Door.Constructor(obj.position), owningBuildingGuid = ownerGuid) @@ -405,7 +414,7 @@ object Zones { // presumably the model is rotated differently to the expected orientation // On top of that, some spawn pads also have an additional rotation (vehiclecreationzorientoffset) // when spawning vehicles set in game_objects.adb.lst - this should be handled on the Scala side - val adjustedYaw = closestSpawnPad.yaw - 90; + val adjustedYaw = closestSpawnPad.yaw - 90 zoneMap.addLocalObject( closestSpawnPad.guid, @@ -537,6 +546,15 @@ object Zones { owningBuildingGuid = ownerGuid ) + case "obbasemesh" => + zoneMap + .addLocalObject( + obj.guid, + OrbitalShuttlePad.Constructor(obj.position, GlobalDefinitions.obbasemesh, Vector3.z(obj.yaw)), + owningBuildingGuid = ownerGuid + ) + zoneMap.linkShuttleToBay(obj.guid) + case _ => () } @@ -561,6 +579,8 @@ object Zones { this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.TR) case "home3" => this.Buildings.values.foreach(_.Faction = PlanetSideEmpire.VS) + case zoneid if zoneid.startsWith("c") => + this.map.cavern = true case _ => () } diff --git a/src/test/scala/Vector3Test.scala b/src/test/scala/Vector3Test.scala index ef39a2bb..3d585e82 100644 --- a/src/test/scala/Vector3Test.scala +++ b/src/test/scala/Vector3Test.scala @@ -205,10 +205,17 @@ class Vector3Test extends Specification { "rotate positive x-axis-vector 90-degrees around the z-axis" in { val A: Vector3 = Vector3(1, 0, 0) A.Rz(0) mustEqual A - A.Rz(90) mustEqual Vector3(0, 1, 0) + A.Rz(90) mustEqual Vector3(0, -1, 0) A.Rz(180) mustEqual Vector3(-1, 0, 0) - A.Rz(270) mustEqual Vector3(0, -1, 0) + A.Rz(270) mustEqual Vector3(0, 1, 0) A.Rz(360) mustEqual A + + val B: Vector3 = Vector3(0, 1, 0) + B.Rz(0) mustEqual B + B.Rz(90) mustEqual Vector3(1, 0, 0) + B.Rz(180) mustEqual Vector3(0, -1, 0) + B.Rz(270) mustEqual Vector3(-1, 0, 0) + B.Rz(360) mustEqual B } "rotate positive y-axis-vector 90-degrees around the x-axis" in { @@ -233,12 +240,12 @@ class Vector3Test extends Specification { val A: Vector3 = Vector3(1, 0, 0) A.Rz(90) .Rx(90) - .Ry(90) mustEqual A + .Ry(-90) mustEqual A } "45-degree rotation" in { val A: Vector3 = Vector3(1, 0, 0) - A.Rz(45) mustEqual Vector3(0.70710677f, 0.70710677f, 0) + A.Rz(45) mustEqual Vector3(0.70710677f, -0.70710677f, 0) } "find a relative up (identity)" in { @@ -257,18 +264,61 @@ class Vector3Test extends Specification { "find a relative up (y-rot)" in { Vector3.relativeUp(Vector3(0, 0, 0)) mustEqual Vector3(0,0,1) //up - Vector3.relativeUp(Vector3(0, 90, 0)) mustEqual Vector3(0,-1,0) //north + Vector3.relativeUp(Vector3(0, 90, 0)) mustEqual Vector3(0,-1,0) //south Vector3.relativeUp(Vector3(0, 180, 0)) mustEqual Vector3(0,0,-1) //down - Vector3.relativeUp(Vector3(0, 270, 0)) mustEqual Vector3(0,1,0) //south + Vector3.relativeUp(Vector3(0, 270, 0)) mustEqual Vector3(0,1,0) //north Vector3.relativeUp(Vector3(0, 360, 0)) mustEqual Vector3(0,0,1) //up } + "find a relative up (x-rot)" in { + Vector3.relativeUp(Vector3(0, 0, 0)) mustEqual Vector3(0,0,1) //up + Vector3.relativeUp(Vector3(90, 0, 0)) mustEqual Vector3(-1,0,0) //west + Vector3.relativeUp(Vector3(180, 0, 0)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(270, 0, 0)) mustEqual Vector3(1,0,0) //east + Vector3.relativeUp(Vector3(360, 0, 0)) mustEqual Vector3(0,0,1) //up + } + "find a relative up (combined y,z)" in { Vector3.relativeUp(Vector3(0, 0, 90)) mustEqual Vector3(0,0,1) //up Vector3.relativeUp(Vector3(0, 90, 90)) mustEqual Vector3(-1,0,0) //west Vector3.relativeUp(Vector3(0, 180, 90)) mustEqual Vector3(0,0,-1) //down Vector3.relativeUp(Vector3(0, 270, 90)) mustEqual Vector3(1,0,0) //east Vector3.relativeUp(Vector3(0, 360, 90)) mustEqual Vector3(0,0,1) //up + + Vector3.relativeUp(Vector3(0, 90, 180)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(0, 180, 180)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(0, 270, 180)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(0, 360, 180)) mustEqual Vector3(0,0,1) //up + + Vector3.relativeUp(Vector3(0, 90, 270)) mustEqual Vector3(1,0,0) //east + Vector3.relativeUp(Vector3(0, 180, 270)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(0, 270, 270)) mustEqual Vector3(-1,0,0) //west + Vector3.relativeUp(Vector3(0, 360, 270)) mustEqual Vector3(0,0,1) //up + } + + "find a relative up (combined x,z)" in { + Vector3.relativeUp(Vector3(0, 0, 90)) mustEqual Vector3(0,0,1) //up + Vector3.relativeUp(Vector3(90, 0, 90)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(180, 0, 90)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(270, 0, 90)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(360, 0, 90)) mustEqual Vector3(0,0,1) //up + + Vector3.relativeUp(Vector3(90, 0, 180)) mustEqual Vector3(1,0,0) //east + Vector3.relativeUp(Vector3(180, 0, 180)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(270, 0, 180)) mustEqual Vector3(-1,0,0) //west + Vector3.relativeUp(Vector3(360, 0, 180)) mustEqual Vector3(0,0,1) //up + + Vector3.relativeUp(Vector3(90, 0, 270)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(180, 0, 270)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(270, 0, 270)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(360, 0, 270)) mustEqual Vector3(0,0,1) //up + } + + "find a relative up (combined x,y)" in { + Vector3.relativeUp(Vector3(0, 90, 0)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(90, 90, 0)) mustEqual Vector3(-1,0,0) //west + Vector3.relativeUp(Vector3(180, 90, 0)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(270, 90, 0)) mustEqual Vector3(1,0,0) //east } } } diff --git a/src/test/scala/base/FreedContextActorTest.scala b/src/test/scala/base/FreedContextActorTest.scala index 070c07f1..36ae3114 100644 --- a/src/test/scala/base/FreedContextActorTest.scala +++ b/src/test/scala/base/FreedContextActorTest.scala @@ -38,7 +38,7 @@ private class ContextSensitive extends Actor { case _ => context.become(PassThroughBehavior) output = sender() - sender() ! context + sender().tell(context, self) } /** diff --git a/src/test/scala/game/CharacterCreateRequestMessageTest.scala b/src/test/scala/game/CharacterCreateRequestMessageTest.scala index a0134a8f..5d841c72 100644 --- a/src/test/scala/game/CharacterCreateRequestMessageTest.scala +++ b/src/test/scala/game/CharacterCreateRequestMessageTest.scala @@ -4,7 +4,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire} import scodec.bits._ class CharacterCreateRequestMessageTest extends Specification { @@ -16,7 +16,7 @@ class CharacterCreateRequestMessageTest extends Specification { name mustEqual "TestChar" head mustEqual 50 voice mustEqual CharacterVoice.Voice5 - gender mustEqual CharacterGender.Female + gender mustEqual CharacterSex.Female faction mustEqual PlanetSideEmpire.NC case _ => ko @@ -25,7 +25,7 @@ class CharacterCreateRequestMessageTest extends Specification { "encode" in { val msg = - CharacterCreateRequestMessage("TestChar", 50, CharacterVoice.Voice5, CharacterGender.Female, PlanetSideEmpire.NC) + CharacterCreateRequestMessage("TestChar", 50, CharacterVoice.Voice5, CharacterSex.Female, PlanetSideEmpire.NC) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string diff --git a/src/test/scala/game/DroppodLaunchRequestMessageTest.scala b/src/test/scala/game/DroppodLaunchRequestMessageTest.scala new file mode 100644 index 00000000..d4de8b53 --- /dev/null +++ b/src/test/scala/game/DroppodLaunchRequestMessageTest.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2021 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.bits._ + +class DroppodLaunchRequestMessageTest extends Specification { + val string = hex"53 2405050000e0b24500c06145c0" + + "DroppodLaunchRequestMessage" should { + "decode" in { + PacketCoding.decodePacket(string).require match { + case DroppodLaunchRequestMessage(info, unk) => + info.guid mustEqual PlanetSideGUID(1316) + info.zone_number mustEqual 5 + info.xypos mustEqual Vector3(5724, 3612, 0) + unk mustEqual 3 + case _ => + ko + } + } + + "encode" in { + val msg = DroppodLaunchRequestMessage(PlanetSideGUID(1316), 5, Vector3(5724, 3612, 0)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string + } + } +} diff --git a/src/test/scala/game/DroppodLaunchResponseMessageTest.scala b/src/test/scala/game/DroppodLaunchResponseMessageTest.scala new file mode 100644 index 00000000..f8385510 --- /dev/null +++ b/src/test/scala/game/DroppodLaunchResponseMessageTest.scala @@ -0,0 +1,92 @@ +// Copyright (c) 2021 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.bits._ + +class DroppodLaunchResponseMessageTest extends Specification { + val string_1 = hex"55 21506000000000000000000000" + val string_2 = hex"55 9150605000000000000000000640000000a0000000" + + "DroppodLaunchResponseMessage" should { + "decode (1)" in { + PacketCoding.decodePacket(string_1).require match { + case DroppodLaunchResponseMessage(error, info, queue) => + error mustEqual DroppodError.BlockedBySOI + info.guid mustEqual PlanetSideGUID(1557) + info.zone_number mustEqual 0 + info.xypos mustEqual Vector3.Zero + queue.isEmpty mustEqual true + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.decodePacket(string_2).require match { + case DroppodLaunchResponseMessage(error, info, queue) => + error mustEqual DroppodError.ZoneFullWarpQueue + info.guid mustEqual PlanetSideGUID(1557) + info.zone_number mustEqual 5 + info.xypos mustEqual Vector3.Zero + queue.contains(WarpQueuePrompt(100, 10)) mustEqual true + case _ => + ko + } + } + + "encode (1)" in { + val msg = DroppodLaunchResponseMessage(DroppodError.BlockedBySOI, PlanetSideGUID(1557), 0, Vector3.Zero) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_1 + } + + "encode (2)" in { + val msg = DroppodLaunchResponseMessage(PlanetSideGUID(1557), 5, 100, 10) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_2 + } + + "invalid constructors" in { + //invalid + DroppodLaunchResponseMessage( + DroppodError.ZoneFullWarpQueue, + DroppodLaunchInfo(PlanetSideGUID(1557), 5, Vector3.Zero), + None + ) must throwA[AssertionError] + + //acceptable + DroppodLaunchResponseMessage( + DroppodError.BlockedBySOI, + DroppodLaunchInfo(PlanetSideGUID(1557), 5, Vector3.Zero), + Some(WarpQueuePrompt(100, 10)) + ) + ok + } + + "equivalent constructors (1)" in { + DroppodLaunchResponseMessage( + DroppodError.BlockedBySOI, + PlanetSideGUID(1557) + ) mustEqual + DroppodLaunchResponseMessage( + DroppodError.BlockedBySOI, + DroppodLaunchInfo(PlanetSideGUID(1557), 0, Vector3.Zero), + None) + } + + "equivalent constructors (2)" in { + DroppodLaunchResponseMessage(PlanetSideGUID(1557), 5, 100, 10) mustEqual + DroppodLaunchResponseMessage( + DroppodError.ZoneFullWarpQueue, + DroppodLaunchInfo(PlanetSideGUID(1557), 5, Vector3.Zero), + Some(WarpQueuePrompt(100, 10)) + ) + } + } +} diff --git a/src/test/scala/game/OrbitalShuttleTimeMsgTest.scala b/src/test/scala/game/OrbitalShuttleTimeMsgTest.scala new file mode 100644 index 00000000..33657559 --- /dev/null +++ b/src/test/scala/game/OrbitalShuttleTimeMsgTest.scala @@ -0,0 +1,222 @@ +// Copyright (c) 2021 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.{HartSequence, PlanetSideGUID} +import scodec.bits._ + +class OrbitalShuttleTimeMsgTest extends Specification { + //these are all from vs sanctuary, near HART A + val string1 = hex"5B 6E3AAE0000200F8000400000000500D9C1051303680414480DA411B0" + val string2 = hex"5B 72A00F8000200F8000400000000500D9C1141303680450480DA41140" + val string3 = hex"5B 62FFFFFFFFA00F8000400000000500D9C1141303680450480DA41140" + val string4 = hex"5B 600000000030750000400000000500D9C1141303680450480DA41140" + val string5 = hex"5B 64F3370180200F8000400000000500D9C1141303680450480DA41140" + val string6 = hex"5B 6953278180200F8000400000000500D9C1061303680464480DA41050" + val string7 = hex"5B 7DCA8D8180200F8000400000000500D9C1141303680450480DA41140" + + "decode (1)" in { + PacketCoding.decodePacket(string1).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.Land + u3 mustEqual 4 + u4 mustEqual 23669 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 5), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 5), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 27) + ) + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.decodePacket(string2).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.PrepareForBoarding + u3 mustEqual 5 + u4 mustEqual 8000 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "decode (3)" in { + PacketCoding.decodePacket(string3).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.State0 + u3 mustEqual 5 + u4 mustEqual 4294967295L + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "decode (4)" in { + PacketCoding.decodePacket(string4).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.State0 + u3 mustEqual 0 + u4 mustEqual 0 + u5 mustEqual 60000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "decode (5)" in { + PacketCoding.decodePacket(string5).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.PrepareForDeparture + u3 mustEqual 1 + u4 mustEqual 224998 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "decode (6)" in { + PacketCoding.decodePacket(string6).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.TakeOff + u3 mustEqual 2 + u4 mustEqual 216998 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 6), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 25), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 5) + ) + case _ => + ko + } + } + + "decode (7)" in { + PacketCoding.decodePacket(string7).require match { + case OrbitalShuttleTimeMsg(u2, u3, u4, u5, u7, u8) => + u2 mustEqual HartSequence.State7 + u3 mustEqual 3 + u4 mustEqual 203669 + u5 mustEqual 8000 + u7 mustEqual 0 + u8 mustEqual List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + ) + case _ => + ko + } + } + + "encode (1)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.Land, 4, 23669, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 5), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 5), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 27) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string1 + } + + "encode (2)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.PrepareForBoarding, 5, 8000, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string2 + } + + "encode (3)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.State0, 5, 4294967295L, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string3 + } + + "encode (4)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.State0, 0, 0, 60000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string4 + } + + "encode (5)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.PrepareForDeparture, 1, 224998, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string5 + } + + "encode (6)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.TakeOff, 2, 216998, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 6), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 25), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 5) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string6 + } + + "encode (7)" in { + val msg = OrbitalShuttleTimeMsg(HartSequence.State7, 3, 203669, 8000, 0, List( + PadAndShuttlePair(PlanetSideGUID(788), PlanetSideGUID(1127), 20), + PadAndShuttlePair(PlanetSideGUID(787), PlanetSideGUID(1128), 20), + PadAndShuttlePair(PlanetSideGUID(786), PlanetSideGUID(1129), 20) + )) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string7 + } +} diff --git a/src/test/scala/game/objectcreate/CharacterDataTest.scala b/src/test/scala/game/objectcreate/CharacterDataTest.scala index 840abcf6..82a7b376 100644 --- a/src/test/scala/game/objectcreate/CharacterDataTest.scala +++ b/src/test/scala/game/objectcreate/CharacterDataTest.scala @@ -40,7 +40,7 @@ class CharacterDataTest extends Specification { case CharacterAppearanceData(a, b, ribbons) => a.app.name mustEqual "ScrawnyRonnie" a.app.faction mustEqual PlanetSideEmpire.TR - a.app.sex mustEqual CharacterGender.Male + a.app.sex mustEqual CharacterSex.Male a.app.head mustEqual 5 a.app.voice mustEqual CharacterVoice.Voice5 a.data.bops mustEqual false @@ -157,7 +157,7 @@ class CharacterDataTest extends Specification { case CharacterAppearanceData(a, b, ribbons) => a.app.name mustEqual "ScrawnyRonnie" a.app.faction mustEqual PlanetSideEmpire.TR - a.app.sex mustEqual CharacterGender.Male + a.app.sex mustEqual CharacterSex.Male a.app.head mustEqual 5 a.app.voice mustEqual CharacterVoice.Voice5 a.data.bops mustEqual false @@ -226,7 +226,7 @@ class CharacterDataTest extends Specification { case CharacterAppearanceData(a, b, ribbons) => a.app.name mustEqual "Angello" a.app.faction mustEqual PlanetSideEmpire.VS - a.app.sex mustEqual CharacterGender.Male + a.app.sex mustEqual CharacterSex.Male a.app.head mustEqual 10 a.app.voice mustEqual CharacterVoice.Voice2 a.data.bops mustEqual false @@ -300,7 +300,7 @@ class CharacterDataTest extends Specification { BasicCharacterData( "ScrawnyRonnie", PlanetSideEmpire.TR, - CharacterGender.Male, + CharacterSex.Male, 5, CharacterVoice.Voice5 ), @@ -408,7 +408,7 @@ class CharacterDataTest extends Specification { BasicCharacterData( "ScrawnyRonnie", PlanetSideEmpire.TR, - CharacterGender.Male, + CharacterSex.Male, 5, CharacterVoice.Voice5 ), @@ -524,7 +524,7 @@ class CharacterDataTest extends Specification { BasicCharacterData( "Angello", PlanetSideEmpire.VS, - CharacterGender.Male, + CharacterSex.Male, 10, CharacterVoice.Voice2 ), diff --git a/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala index 8b28c142..ce539a45 100644 --- a/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala +++ b/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala @@ -70,7 +70,7 @@ class DetailedCharacterDataTest extends Specification { case CharacterAppearanceData(a, b, ribbons) => a.app.name mustEqual "IlllIIIlllIlIllIlllIllI" a.app.faction mustEqual PlanetSideEmpire.VS - a.app.sex mustEqual CharacterGender.Female + a.app.sex mustEqual CharacterSex.Female a.app.head mustEqual 41 a.app.voice mustEqual CharacterVoice.Voice1 a.data.bops mustEqual false @@ -258,7 +258,7 @@ class DetailedCharacterDataTest extends Specification { case CharacterAppearanceData(a, b, ribbons) => a.app.name mustEqual "IlllIIIlllIlIllIlllIllI" a.app.faction mustEqual PlanetSideEmpire.VS - a.app.sex mustEqual CharacterGender.Female + a.app.sex mustEqual CharacterSex.Female a.app.head mustEqual 41 a.app.voice mustEqual CharacterVoice.Voice1 a.data.bops mustEqual false @@ -443,7 +443,7 @@ class DetailedCharacterDataTest extends Specification { case CharacterAppearanceData(a, b, ribbons) => a.app.name mustEqual "HaHaATRMax" a.app.faction mustEqual PlanetSideEmpire.TR - a.app.sex mustEqual CharacterGender.Male + a.app.sex mustEqual CharacterSex.Male a.app.head mustEqual 57 a.app.voice mustEqual CharacterVoice.Voice1 a.data.bops mustEqual false @@ -670,7 +670,7 @@ class DetailedCharacterDataTest extends Specification { case CharacterAppearanceData(a, b, ribbons) => a.app.name mustEqual "KiCkJr" a.app.faction mustEqual PlanetSideEmpire.NC - a.app.sex mustEqual CharacterGender.Male + a.app.sex mustEqual CharacterSex.Male a.app.head mustEqual 24 a.app.voice mustEqual CharacterVoice.Voice4 a.data.bops mustEqual false @@ -1189,7 +1189,7 @@ class DetailedCharacterDataTest extends Specification { a.app mustEqual BasicCharacterData( "CCRIDER", PlanetSideEmpire.NC, - CharacterGender.Male, + CharacterSex.Male, 20, CharacterVoice.Voice3 ) @@ -1336,7 +1336,7 @@ class DetailedCharacterDataTest extends Specification { a.app mustEqual BasicCharacterData( "xRider912", PlanetSideEmpire.TR, - CharacterGender.Male, + CharacterSex.Male, 4, CharacterVoice.Voice1 ) @@ -1515,7 +1515,7 @@ class DetailedCharacterDataTest extends Specification { BasicCharacterData( "IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 41, CharacterVoice.Voice1 ), @@ -1697,7 +1697,7 @@ class DetailedCharacterDataTest extends Specification { BasicCharacterData( "IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 41, CharacterVoice.Voice1 ), @@ -1886,7 +1886,7 @@ class DetailedCharacterDataTest extends Specification { BasicCharacterData( "HaHaATRMax", PlanetSideEmpire.TR, - CharacterGender.Male, + CharacterSex.Male, 57, CharacterVoice.Voice1 ), @@ -2103,7 +2103,7 @@ class DetailedCharacterDataTest extends Specification { BasicCharacterData( "KiCkJr", PlanetSideEmpire.NC, - CharacterGender.Male, + CharacterSex.Male, 24, CharacterVoice.Voice4 ), @@ -3588,7 +3588,7 @@ class DetailedCharacterDataTest extends Specification { BasicCharacterData( "CCRIDER", PlanetSideEmpire.NC, - CharacterGender.Male, + CharacterSex.Male, 20, CharacterVoice.Voice3 ), @@ -4512,7 +4512,7 @@ class DetailedCharacterDataTest extends Specification { BasicCharacterData( "xRider912", PlanetSideEmpire.TR, - CharacterGender.Male, + CharacterSex.Male, 4, CharacterVoice.Voice1 ), diff --git a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala index 7b45ed48..9a85310d 100644 --- a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala +++ b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala @@ -57,7 +57,7 @@ class MountedVehiclesTest extends Specification { a.app mustEqual BasicCharacterData( "ScrawnyRonnie", PlanetSideEmpire.TR, - CharacterGender.Male, + CharacterSex.Male, 5, CharacterVoice.Voice5 ) @@ -146,7 +146,7 @@ class MountedVehiclesTest extends Specification { BasicCharacterData( "ScrawnyRonnie", PlanetSideEmpire.TR, - CharacterGender.Male, + CharacterSex.Male, 5, CharacterVoice.Voice5 ), diff --git a/src/test/scala/objects/AvatarTest.scala b/src/test/scala/objects/AvatarTest.scala index a5a4c47d..7a7dbbc7 100644 --- a/src/test/scala/objects/AvatarTest.scala +++ b/src/test/scala/objects/AvatarTest.scala @@ -6,12 +6,12 @@ import net.psforever.objects._ import net.psforever.objects.avatar.{Avatar, BattleRank, Implant} import net.psforever.objects.definition.ImplantDefinition import net.psforever.objects.locker.LockerEquipment -import net.psforever.types.{CharacterGender, CharacterVoice, ImplantType, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, ImplantType, PlanetSideEmpire} import org.specs2.mutable._ class AvatarTest extends Specification { def CreatePlayer(): (Player, Avatar) = { - val avatar = Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1) + val avatar = Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterSex.Female, 41, CharacterVoice.Voice1) val player = Player(avatar) player.Slot(0).Equipment = Tool(beamer) player.Slot(2).Equipment = Tool(suppressor) @@ -26,10 +26,10 @@ class AvatarTest extends Specification { } "construct" in { - val av = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val av = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) av.name mustEqual "Chord" av.faction mustEqual PlanetSideEmpire.TR - av.sex mustEqual CharacterGender.Male + av.sex mustEqual CharacterSex.Male av.head mustEqual 0 av.voice mustEqual CharacterVoice.Voice5 av.bep mustEqual 0 @@ -39,7 +39,7 @@ class AvatarTest extends Specification { } "can not maintain experience point values below zero" in { - val av = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val av = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) av.bep mustEqual 0 av.copy(bep = -1) must throwA[AssertionError] av.copy(cep = -1) must throwA[AssertionError] @@ -47,7 +47,7 @@ class AvatarTest extends Specification { //refer to ImplantTest.scala for more tests "maximum of three implant slots" in { - val obj = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.implants.length mustEqual 3 obj.implants(0) must beNone obj.implants(1) must beNone @@ -61,7 +61,7 @@ class AvatarTest extends Specification { 0, "Chord", PlanetSideEmpire.TR, - CharacterGender.Male, + CharacterSex.Male, 0, CharacterVoice.Voice5, bep = BattleRank.BR6.experience @@ -81,7 +81,7 @@ class AvatarTest extends Specification { "can not install the same type of implant twice" in { val testplant1 = Implant(new ImplantDefinition(ImplantType.AdvancedRegen)) val testplant2 = Implant(new ImplantDefinition(ImplantType.AdvancedRegen)) - val obj = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.copy(implants = obj.implants.updated(0, Some(testplant1)).updated(1, Some(testplant2))) must throwA[ AssertionError ] @@ -91,7 +91,7 @@ class AvatarTest extends Specification { val testplant1 = Implant(new ImplantDefinition(ImplantType.AdvancedRegen)) val testplant2 = Implant(new ImplantDefinition(ImplantType.Surge)) val testplant3 = Implant(new ImplantDefinition(ImplantType.DarklightVision)) - val obj = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.copy( bep = BattleRank.BR12.value, implants = Seq(Some(testplant1), Some(testplant2), Some(testplant3)) diff --git a/src/test/scala/objects/ConverterTest.scala b/src/test/scala/objects/ConverterTest.scala index 3f26716c..6938a734 100644 --- a/src/test/scala/objects/ConverterTest.scala +++ b/src/test/scala/objects/ConverterTest.scala @@ -8,6 +8,7 @@ import net.psforever.objects.definition._ import net.psforever.objects.equipment._ import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.locker.{LockerContainer, LockerEquipment} +import net.psforever.objects.serverobject.mount.{MountInfo, SeatDefinition} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles.UtilityType @@ -647,7 +648,7 @@ class ConverterTest extends Specification { } "Player" should { - var avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + var avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val obj: Player = { /* Create an AmmoBoxDefinition with which to build two AmmoBoxes @@ -872,10 +873,10 @@ class ConverterTest extends Specification { val fury_def = VehicleDefinition(ObjectClass.fury) fury_def.Seats += 0 -> new SeatDefinition() - fury_def.Seats(0).Bailable = true - fury_def.Seats(0).ControlledWeapon = Some(1) - fury_def.MountPoints += 0 -> 0 - fury_def.MountPoints += 2 -> 0 + fury_def.Seats(0).bailable = true + fury_def.controlledWeapons += 0 -> 1 + fury_def.MountPoints += 0 -> MountInfo(0) + fury_def.MountPoints += 2 -> MountInfo(0) fury_def.Weapons += 1 -> fury_weapon_systema_def fury_def.TrunkSize = InventoryTile(11, 11) fury_def.TrunkOffset = 30 diff --git a/src/test/scala/objects/DamageModelTests.scala b/src/test/scala/objects/DamageModelTests.scala index 352c2fbd..13f60d95 100644 --- a/src/test/scala/objects/DamageModelTests.scala +++ b/src/test/scala/objects/DamageModelTests.scala @@ -26,7 +26,7 @@ class DamageCalculationsTests extends Specification { val wep_prof = wep_fmode.Add val proj = DamageModelTests.projectile val proj_prof = proj.asInstanceOf[DamageProfile] - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero) val target = Vehicle(GlobalDefinitions.fury) target.Position = Vector3(10, 0, 0) @@ -450,7 +450,7 @@ class DamageCalculationsTests extends Specification { "galaxy gunship reduction (target is not a vehicle)" in { val tplayer = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val resfprojectile = DamageInteraction( SourceEntry(tplayer), ProjectileReason( @@ -479,7 +479,7 @@ class ResistanceCalculationsTests extends Specification { val wep = GlobalDefinitions.galaxy_gunship_cannon val wep_fmode = Tool(wep).FireMode val proj = DamageModelTests.projectile - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero) "ResistanceCalculations" should { @@ -515,7 +515,7 @@ class ResistanceCalculationsTests extends Specification { } "discern mechanized infantry targets" in { - val target = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val target = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) target.ExoSuit = ExoSuitType.MAX val resprojectile = DamageInteraction( SourceEntry(target), @@ -592,7 +592,7 @@ class ResolutionCalculationsTests extends Specification { val wep = GlobalDefinitions.galaxy_gunship_cannon val wep_fmode = Tool(wep).FireMode val proj = DamageModelTests.projectile - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero) @@ -656,7 +656,7 @@ class ResolutionCalculationsTests extends Specification { InfantryDamageAfterResist(100, 100)(50, 60) mustEqual (0, 50) } - val player2 = Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player2 = Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player2.ExoSuit = ExoSuitType.MAX player2.Spawn() "calculate no max damage for vehicles" in { @@ -740,7 +740,7 @@ class DamageModelTests extends Specification { val wep_tool = Tool(wep) val wep_fmode = wep_tool.FireMode val proj = DamageModelTests.projectile - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) player.Spawn() val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero) @@ -767,7 +767,7 @@ class DamageModelTests extends Specification { "resolve infantry targets" in { val tplayer = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) tplayer.Spawn() tplayer.Health mustEqual 100 tplayer.Armor mustEqual 50 @@ -789,7 +789,7 @@ class DamageModelTests extends Specification { "resolve infantry targets in a specific way" in { val tplayer = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) tplayer.Spawn() tplayer.Health mustEqual 100 tplayer.Armor mustEqual 50 @@ -811,7 +811,7 @@ class DamageModelTests extends Specification { "resolve infantry targets, with damage overflow" in { val tplayer = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) tplayer.Spawn() tplayer.Health mustEqual 100 tplayer.Armor mustEqual 50 diff --git a/src/test/scala/objects/DamageableTest.scala b/src/test/scala/objects/DamageableTest.scala index d3c9e062..4809d153 100644 --- a/src/test/scala/objects/DamageableTest.scala +++ b/src/test/scala/objects/DamageableTest.scala @@ -35,7 +35,7 @@ import net.psforever.objects.vital.base.DamageResolution import net.psforever.objects.vital.projectile.ProjectileReason class DamageableTest extends Specification { - val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val pSource = PlayerSource(player1) val weaponA = Tool(GlobalDefinitions.phoenix) //decimator val projectileA = weaponA.Projectile @@ -128,7 +128,7 @@ class DamageableTest extends Specification { "permit damaging friendly targets, even those not designated for friendly fire, if the target is hacked" in { val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) player2.GUID = PlanetSideGUID(1) val target = new Terminal(new TerminalDefinition(0) { Damageable = true @@ -240,7 +240,7 @@ class DamageableTest extends Specification { "permit jamming friendly targets if the target is hacked" in { val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) player2.GUID = PlanetSideGUID(1) val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) target.Faction = player1.Faction @@ -285,7 +285,7 @@ class DamageableEntityDamageTest extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val gen = Generator(GlobalDefinitions.generator) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 guid.register(building, 1) guid.register(gen, 2) guid.register(player1, 3) @@ -355,7 +355,7 @@ class DamageableEntityDestroyedTest extends ActorTest { mech.Position = Vector3(1, 0, 0) mech.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], mech), "mech-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Position = Vector3(14, 0, 0) //<14m from generator; dies player1.Spawn() val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 @@ -426,7 +426,7 @@ class DamageableEntityNotDestroyTwice extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val gen = Generator(GlobalDefinitions.generator) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() guid.register(building, 1) guid.register(gen, 2) @@ -499,7 +499,7 @@ class DamageableAmenityTest extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val term = Terminal(GlobalDefinitions.order_terminal) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() guid.register(building, 1) guid.register(term, 2) @@ -589,11 +589,11 @@ class DamageableMountableDamageTest extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val mech = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player2.Spawn() guid.register(building, 1) guid.register(mech, 2) @@ -631,8 +631,8 @@ class DamageableMountableDamageTest extends ActorTest { Vector3(1, 0, 0) ) val applyDamageTo = resolved.calculate() - mech.Seats(0).Occupant = player2 //seat the player - player2.VehicleSeated = Some(mech.GUID) //seat the player + mech.Seats(0).mount(player2) //mount the player + player2.VehicleSeated = Some(mech.GUID) //mount the player expectNoMessage(200 milliseconds) //we're not testing that the math is correct @@ -683,13 +683,13 @@ class DamageableMountableDestroyTest extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val mech = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref @@ -728,8 +728,8 @@ class DamageableMountableDestroyTest extends ActorTest { Vector3(1, 0, 0) ) val applyDamageTo = resolved.calculate() - mech.Seats(0).Occupant = player2 //seat the player - player2.VehicleSeated = Some(mech.GUID) //seat the player + mech.Seats(0).mount(player2) //mount the player + player2.VehicleSeated = Some(mech.GUID) //mount the player expectNoMessage(200 milliseconds) //we're not testing that the math is correct @@ -784,20 +784,20 @@ class DamageableWeaponTurretDamageTest extends ActorTest { turret.Zone = zone turret.Position = Vector3(1, 0, 0) val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref guid.register(turret, 2) guid.register(player1, 3) guid.register(player2, 4) - turret.Seats(0).Occupant = player2 + turret.Seats(0).mount(player2) player2.VehicleSeated = turret.GUID val weapon = Tool(GlobalDefinitions.suppressor) @@ -882,13 +882,13 @@ class DamageableWeaponTurretJammerTest extends ActorTest { val turretWeapon = turret.Weapons.values.head.Equipment.get.asInstanceOf[Tool] val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref @@ -898,7 +898,7 @@ class DamageableWeaponTurretJammerTest extends ActorTest { guid.register(player2, 4) guid.register(turretWeapon, 5) guid.register(turretWeapon.AmmoSlot.Box, 6) - turret.Seats(0).Occupant = player2 + turret.Seats(0).mount(player2) player2.VehicleSeated = turret.GUID val weapon = Tool(GlobalDefinitions.jammer_grenade) @@ -982,13 +982,13 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { val turretWeapon = turret.Weapons.values.head.Equipment.get.asInstanceOf[Tool] val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref @@ -999,7 +999,7 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { guid.register(player2, 4) guid.register(turretWeapon, 5) guid.register(turretWeapon.AmmoSlot.Box, 6) - turret.Seats(0).Occupant = player2 + turret.Seats(0).mount(player2) player2.VehicleSeated = turret.GUID building.Position = Vector3(1, 0, 0) building.Zone = zone @@ -1060,7 +1060,7 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { assert(!turret.Destroyed) turret.Actor ! Vitality.Damage(applyDamageToA) //also test destruction while jammered - vehicleProbe.receiveN(2, 500 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) @@ -1133,13 +1133,13 @@ class DamageableVehicleDamageTest extends ActorTest { atv.Position = Vector3(1, 0, 0) val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=2 player1.Spawn() player1.Position = Vector3(2, 0, 0) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref @@ -1148,7 +1148,7 @@ class DamageableVehicleDamageTest extends ActorTest { guid.register(player1, 2) guid.register(player2, 3) atv.Zone = zone - atv.Seats(0).Occupant = player2 + atv.Seats(0).mount(player2) player2.VehicleSeated = atv.GUID val weapon = Tool(GlobalDefinitions.suppressor) @@ -1242,18 +1242,18 @@ class DamageableVehicleDamageMountedTest extends ActorTest { atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "atv-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=2 player1.Spawn() player1.Position = Vector3(2, 0, 0) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref val player3 = - Player(Avatar(0, "TestCharacter3", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=10 + Player(Avatar(0, "TestCharacter3", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=10 player3.Spawn() val player3Probe = TestProbe() player3.Actor = player3Probe.ref @@ -1273,12 +1273,12 @@ class DamageableVehicleDamageMountedTest extends ActorTest { //the lodestar control actor needs to load after the utilities have guid's assigned lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") lodestar.Zone = zone - lodestar.Seats(0).Occupant = player2 + lodestar.Seats(0).mount(player2) player2.VehicleSeated = lodestar.GUID atv.Zone = zone - atv.Seats(0).Occupant = player3 + atv.Seats(0).mount(player3) player3.VehicleSeated = atv.GUID - lodestar.CargoHolds(1).Occupant = atv + lodestar.CargoHolds(1).mount(atv) atv.MountedIn = lodestar.GUID val weapon = Tool(GlobalDefinitions.phoenix) //decimator @@ -1387,18 +1387,18 @@ class DamageableVehicleJammeringMountedTest extends ActorTest { lodestar.Position = Vector3(1, 0, 0) val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=7 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=7 player1.Spawn() player1.Position = Vector3(2, 0, 0) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=8 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=8 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref val player3 = - Player(Avatar(0, "TestCharacter3", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=9 + Player(Avatar(0, "TestCharacter3", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=9 player3.Spawn() val player3Probe = TestProbe() player3.Actor = player3Probe.ref @@ -1420,11 +1420,11 @@ class DamageableVehicleJammeringMountedTest extends ActorTest { lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") atv.Zone = zone lodestar.Zone = zone - atv.Seats(0).Occupant = player2 + atv.Seats(0).mount(player2) player2.VehicleSeated = atv.GUID - lodestar.Seats(0).Occupant = player3 + lodestar.Seats(0).mount(player3) player3.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = atv + lodestar.CargoHolds(1).mount(atv) atv.MountedIn = lodestar.GUID val vehicleSource = SourceEntry(lodestar) @@ -1499,13 +1499,13 @@ class DamageableVehicleDestroyTest extends ActorTest { val atvWeapon = atv.Weapons(1).Equipment.get.asInstanceOf[Tool] //guid=4 & 5 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=2 player1.Spawn() player1.Position = Vector3(2, 0, 0) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref @@ -1516,7 +1516,7 @@ class DamageableVehicleDestroyTest extends ActorTest { guid.register(atvWeapon, 4) guid.register(atvWeapon.AmmoSlot.Box, 5) atv.Zone = zone - atv.Seats(0).Occupant = player2 + atv.Seats(0).mount(player2) player2.VehicleSeated = atv.GUID val weapon = Tool(GlobalDefinitions.suppressor) @@ -1595,18 +1595,18 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { lodestar.Position = Vector3(1, 0, 0) val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=7 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=7 player1.Spawn() player1.Position = Vector3(2, 0, 0) val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=8 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=8 player2.Spawn() val player2Probe = TestProbe() player2.Actor = player2Probe.ref val player3 = - Player(Avatar(0, "TestCharacter3", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=9 + Player(Avatar(0, "TestCharacter3", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=9 player3.Spawn() val player3Probe = TestProbe() player3.Actor = player3Probe.ref @@ -1642,11 +1642,11 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") atv.Zone = zone lodestar.Zone = zone - atv.Seats(0).Occupant = player2 + atv.Seats(0).mount(player2) player2.VehicleSeated = atv.GUID - lodestar.Seats(0).Occupant = player3 + lodestar.Seats(0).mount(player3) player3.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = atv + lodestar.CargoHolds(1).mount(atv) atv.MountedIn = lodestar.GUID val vehicleSource = SourceEntry(lodestar) diff --git a/src/test/scala/objects/DeployableTest.scala b/src/test/scala/objects/DeployableTest.scala index 1fa1db7b..f8eca34b 100644 --- a/src/test/scala/objects/DeployableTest.scala +++ b/src/test/scala/objects/DeployableTest.scala @@ -8,7 +8,7 @@ import net.psforever.objects.ballistics._ import net.psforever.objects.ce.DeployedItem import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.mount.{MountInfo, Mountable} import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects.{TurretDeployable, _} @@ -156,8 +156,11 @@ class TurretDeployableTest extends Specification { } "may have mount point" in { - new TurretDeployable(GlobalDefinitions.spitfire_turret).MountPoints mustEqual Map() - new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs).MountPoints mustEqual Map(1 -> 0, 2 -> 0) + new TurretDeployable(GlobalDefinitions.spitfire_turret).MountPoints.isEmpty mustEqual true + + val pmt = new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs) + pmt.MountPoints.get(1).contains(MountInfo(0, Vector3(0, 0, 0))) mustEqual true + pmt.MountPoints.get(2).contains(MountInfo(0, Vector3(0, 0, 0))) mustEqual true } } } @@ -317,10 +320,10 @@ class ExplosiveDeployableJammerTest extends ActorTest { val j_mine = Deployables.Make(DeployedItem.jammer_mine)().asInstanceOf[ExplosiveDeployable] //guid=1 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player2.Spawn() val weapon = Tool(GlobalDefinitions.jammer_grenade) //guid=5 guid.register(j_mine, 1) @@ -417,10 +420,10 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest { val h_mine = Deployables.Make(DeployedItem.he_mine)().asInstanceOf[ExplosiveDeployable] //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player2.Spawn() val weapon = Tool(GlobalDefinitions.jammer_grenade) //guid=5 guid.register(h_mine, 2) @@ -528,10 +531,10 @@ class ExplosiveDeployableDestructionTest extends ActorTest { val h_mine = Deployables.Make(DeployedItem.he_mine)().asInstanceOf[ExplosiveDeployable] //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player2.Spawn() val weapon = Tool(GlobalDefinitions.suppressor) //guid=5 guid.register(h_mine, 2) @@ -669,15 +672,15 @@ class TurretControlMountTest extends ActorTest { obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") - assert(obj.Seats(0).Occupant.isEmpty) - val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player1, 0) + assert(obj.Seats(0).occupant.isEmpty) + val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player1, 1) val reply1a = receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player1) assert(reply1b.response.isInstanceOf[Mountable.CanMount]) - assert(obj.Seats(0).Occupant.contains(player1)) + assert(obj.Seats(0).occupant.contains(player1)) } } } @@ -689,18 +692,18 @@ class TurretControlBlockMountTest extends ActorTest { obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") - assert(obj.Seats(0).Occupant.isEmpty) - val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player1, 0) + assert(obj.Seats(0).occupant.isEmpty) + val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player1, 1) val reply1a = receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player1) assert(reply1b.response.isInstanceOf[Mountable.CanMount]) - assert(obj.Seats(0).Occupant.contains(player1)) + assert(obj.Seats(0).occupant.contains(player1)) - val player2 = Player(Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player2, 0) + val player2 = Player(Avatar(1, "test2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player2, 1) val reply2a = receiveOne(200 milliseconds) assert(reply2a.isInstanceOf[Mountable.MountMessages]) val reply2b = reply2a.asInstanceOf[Mountable.MountMessages] @@ -717,15 +720,15 @@ class TurretControlBlockBetrayalMountTest extends ActorTest { obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") - assert(obj.Seats(0).Occupant.isEmpty) - val player = Player(Avatar(0, "test", PlanetSideEmpire.VS, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player, 0) + assert(obj.Seats(0).occupant.isEmpty) + val player = Player(Avatar(0, "test", PlanetSideEmpire.VS, CharacterSex.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player, 1) val reply1a = receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player) assert(reply1b.response.isInstanceOf[Mountable.CanNotMount]) - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) } } } @@ -737,15 +740,16 @@ class TurretControlDismountTest extends ActorTest { obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") - assert(obj.Seats(0).Occupant.isEmpty) - val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - obj.Actor ! Mountable.TryMount(player, 0) + assert(obj.Seats(0).occupant.isEmpty) + val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + obj.Actor ! Mountable.TryMount(player, 1) + val reply1a = receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player) assert(reply1b.response.isInstanceOf[Mountable.CanMount]) - assert(obj.Seats(0).Occupant.contains(player)) + assert(obj.Seats(0).occupant.contains(player)) obj.Actor ! Mountable.TryDismount(player, 0) val reply2a = receiveOne(200 milliseconds) @@ -753,7 +757,7 @@ class TurretControlDismountTest extends ActorTest { val reply2b = reply2a.asInstanceOf[Mountable.MountMessages] assert(reply2b.player == player) assert(reply2b.response.isInstanceOf[Mountable.CanDismount]) - assert(obj.Seats(0).Occupant.isEmpty) + assert(obj.Seats(0).occupant.isEmpty) } } } @@ -762,21 +766,25 @@ class TurretControlBetrayalMountTest extends ActorTest { "TurretControl" should { "allow all allegiances" in { val obj = new TurretDeployable( - new TurretDeployableDefinition(685) { FactionLocked = false } //required (defaults to true) + new TurretDeployableDefinition(685) { + MountPoints += 1 -> MountInfo(0, Vector3.Zero) + FactionLocked = false + } //required (defaults to true) ) { GUID = PlanetSideGUID(1) } obj.Faction = PlanetSideEmpire.TR obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test") + val probe = new TestProbe(system) - assert(obj.Seats(0).Occupant.isEmpty) - val player = Player(Avatar(0, "test", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) + assert(obj.Seats(0).occupant.isEmpty) + val player = Player(Avatar(0, "test", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) assert(player.Faction != obj.Faction) - obj.Actor ! Mountable.TryMount(player, 0) - val reply1a = receiveOne(200 milliseconds) + obj.Actor.tell(Mountable.TryMount(player, 1), probe.ref) + val reply1a = probe.receiveOne(200 milliseconds) assert(reply1a.isInstanceOf[Mountable.MountMessages]) val reply1b = reply1a.asInstanceOf[Mountable.MountMessages] assert(reply1b.player == player) assert(reply1b.response.isInstanceOf[Mountable.CanMount]) - assert(obj.Seats(0).Occupant.contains(player)) + assert(obj.Seats(0).occupant.contains(player)) } } } diff --git a/src/test/scala/objects/DoorTest.scala b/src/test/scala/objects/DoorTest.scala index bc908e66..72e08daf 100644 --- a/src/test/scala/objects/DoorTest.scala +++ b/src/test/scala/objects/DoorTest.scala @@ -20,7 +20,7 @@ import org.specs2.mutable.Specification import scala.concurrent.duration._ class DoorTest extends Specification { - val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) "Door" should { "construct" in { @@ -148,7 +148,7 @@ object DoorControlTest { GlobalDefinitions.building ) door.Owner.Faction = faction - val player = Player(Avatar(0, "test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute)) guid.register(player, 2) (player, door) } diff --git a/src/test/scala/objects/EnvironmentTest.scala b/src/test/scala/objects/EnvironmentTest.scala index 51613ef1..0ee7e3c2 100644 --- a/src/test/scala/objects/EnvironmentTest.scala +++ b/src/test/scala/objects/EnvironmentTest.scala @@ -1,13 +1,14 @@ // Copyright (c) 2020 PSForever package objects +import net.psforever.objects.avatar.Avatar import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle} import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.terminals.{Terminal, TerminalDefinition} import net.psforever.objects.vital.Vitality import net.psforever.packet.game.objectcreate.ObjectClass -import net.psforever.types.Vector3 +import net.psforever.types._ import org.specs2.mutable.Specification class EnvironmentCollisionTest extends Specification { @@ -203,6 +204,26 @@ class EnvironmentAttributeTest extends Specification { EnvironmentAttribute.Death.canInteractWith(obj) mustEqual false } } + + "GantryDenialField" should { + "interact with players" in { + val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + obj.Spawn() + EnvironmentAttribute.GantryDenialField.canInteractWith(obj) mustEqual true + } + + "not interact with dead players" in { + val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + obj.isAlive mustEqual false + EnvironmentAttribute.GantryDenialField.canInteractWith(obj) mustEqual false + } + + "not interact with an object that is not a player" in { + val obj = Tool(GlobalDefinitions.suppressor) + obj.isInstanceOf[Vitality] mustEqual false + EnvironmentAttribute.GantryDenialField.canInteractWith(obj) mustEqual false + } + } } class SeaLevelTest extends Specification { @@ -281,6 +302,17 @@ class PoolTest extends Specification { } } +class GantryDenialField extends Specification { + val square = DeepSquare(0, 1, 10, 10, 1) + + "GantryDenialField" should { + "always has the environmental attribute of 'GantryDenialField'" in { + val obj = GantryDenialField(PlanetSideGUID(0), 0, square) + obj.attribute mustEqual EnvironmentAttribute.GantryDenialField + } + } +} + class PieceOfEnvironmentTest extends Specification { "PieceOfEnvironment" should { import PieceOfEnvironment.testStepIntoInteraction diff --git a/src/test/scala/objects/FacilityTurretTest.scala b/src/test/scala/objects/FacilityTurretTest.scala index 45330390..90e0e944 100644 --- a/src/test/scala/objects/FacilityTurretTest.scala +++ b/src/test/scala/objects/FacilityTurretTest.scala @@ -10,7 +10,7 @@ import net.psforever.objects.definition.ToolDefinition import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.mount.{MountInfo, Mountable} import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.turret._ import net.psforever.objects.zones.{Zone, ZoneMap} @@ -31,7 +31,7 @@ class FacilityTurretTest extends Specification { obj.ReserveAmmunition mustEqual false obj.FactionLocked mustEqual true obj.MaxHealth mustEqual 0 - obj.MountPoints mustEqual mutable.HashMap.empty[Int, Int] + obj.MountPoints.isEmpty mustEqual true } "construct" in { @@ -44,9 +44,8 @@ class FacilityTurretTest extends Specification { ko } obj.Seats.size mustEqual 1 - obj.Seats(0).ControlledWeapon.contains(1) mustEqual true obj.MountPoints.size mustEqual 1 - obj.MountPoints(1) mustEqual 0 + obj.MountPoints.get(1).contains(MountInfo(0, Vector3.Zero)) mustEqual true obj.Health mustEqual 3600 obj.Upgrade mustEqual TurretUpgrade.None obj.Health = 360 @@ -101,7 +100,7 @@ class FacilityTurretControl1Test extends ActorTest { } class FacilityTurretControl2Test extends ActorTest { - val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj = FacilityTurret(GlobalDefinitions.manned_turret) obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control") @@ -110,12 +109,12 @@ class FacilityTurretControl2Test extends ActorTest { bldg.Faction = PlanetSideEmpire.TR "FacilityTurretControl" should { - "seat on faction affiliation when FactionLock is true" in { + "mount on faction affiliation when FactionLock is true" in { assert(player.Faction == PlanetSideEmpire.TR) assert(obj.Faction == PlanetSideEmpire.TR) assert(obj.Definition.FactionLocked) - obj.Actor ! Mountable.TryMount(player, 0) + obj.Actor ! Mountable.TryMount(player, 1) val reply = receiveOne(300 milliseconds) reply match { case msg: Mountable.MountMessages => @@ -128,7 +127,7 @@ class FacilityTurretControl2Test extends ActorTest { } class FacilityTurretControl3Test extends ActorTest { - val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj = FacilityTurret(GlobalDefinitions.manned_turret) obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control") @@ -141,7 +140,7 @@ class FacilityTurretControl3Test extends ActorTest { assert(obj.Faction == PlanetSideEmpire.NEUTRAL) assert(obj.Definition.FactionLocked) - obj.Actor ! Mountable.TryMount(player, 0) + obj.Actor ! Mountable.TryMount(player, 1) val reply = receiveOne(300 milliseconds) reply match { case msg: Mountable.MountMessages => @@ -154,10 +153,8 @@ class FacilityTurretControl3Test extends ActorTest { } class FacilityTurretControl4Test extends ActorTest { - val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - val objDef = new FacilityTurretDefinition(480) - objDef.FactionLocked = false - val obj = FacilityTurret(objDef) + val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + val obj = FacilityTurret(GlobalDefinitions.vanu_sentry_turret) obj.GUID = PlanetSideGUID(1) obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control") val bldg = Building("Building", guid = 0, map_id = 0, Zone.Nowhere, StructureType.Building) @@ -169,7 +166,7 @@ class FacilityTurretControl4Test extends ActorTest { assert(obj.Faction == PlanetSideEmpire.NEUTRAL) assert(!obj.Definition.FactionLocked) - obj.Actor ! Mountable.TryMount(player, 0) + obj.Actor ! Mountable.TryMount(player, 1) val reply = receiveOne(300 milliseconds) reply match { case msg: Mountable.MountMessages => @@ -204,7 +201,7 @@ class FacilityTurretControlRestorationTest extends ActorTest { val turretWeapon = turret.Weapons.values.head.Equipment.get.asInstanceOf[Tool] val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player1Probe = TestProbe() diff --git a/src/test/scala/objects/GeneratorTest.scala b/src/test/scala/objects/GeneratorTest.scala index 8a951699..5dab52a7 100644 --- a/src/test/scala/objects/GeneratorTest.scala +++ b/src/test/scala/objects/GeneratorTest.scala @@ -66,7 +66,7 @@ class GeneratorControlDamageTest extends ActorTest { gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Position = Vector3(14, 0, 0) //<14m from generator; dies player1.Spawn() @@ -149,7 +149,7 @@ class GeneratorControlCriticalTest extends ActorTest { gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Position = Vector3(14, 0, 0) //<14m from generator; dies player1.Spawn() @@ -234,7 +234,7 @@ class GeneratorControlDestroyedTest extends ActorTest { gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Position = Vector3(14, 0, 0) //<14m from generator; dies player1.Spawn() player1.Actor = TestProbe().ref @@ -343,13 +343,13 @@ class GeneratorControlKillsTest extends ActorTest { gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Position = Vector3(14, 0, 0) //<14m from generator; dies player1.Spawn() val player1Probe = TestProbe() player1.Actor = player1Probe.ref val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Female, 1, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Female, 1, CharacterVoice.Mute)) //guid=4 player2.Position = Vector3(25, 0, 0) //>14m from generator; lives player2.Spawn() val player2Probe = TestProbe() @@ -481,7 +481,7 @@ class GeneratorControlNotDestroyTwice extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val gen = Generator(GeneratorTest.generator_definition) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() guid.register(building, 1) guid.register(gen, 2) @@ -572,7 +572,7 @@ class GeneratorControlNotDamageIfExplodingTest extends ActorTest { gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Position = Vector3(14, 0, 0) //<14m from generator; dies player1.Spawn() val player1Probe = TestProbe() @@ -671,7 +671,7 @@ class GeneratorControlNotRepairIfExplodingTest extends ActorTest { gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Position = Vector3(14, 0, 0) //<14m from generator; dies player1.Spawn() val player1Probe = TestProbe() @@ -774,7 +774,7 @@ class GeneratorControlRepairPastRestorePoint extends ActorTest { gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Position = Vector3(14, 0, 0) //<14m from generator; dies player1.Spawn() val player1Probe = TestProbe() diff --git a/src/test/scala/objects/GeometryTest.scala b/src/test/scala/objects/GeometryTest.scala new file mode 100644 index 00000000..05640453 --- /dev/null +++ b/src/test/scala/objects/GeometryTest.scala @@ -0,0 +1,244 @@ +// Copyright (c) 2021 PSForever +package objects + +import net.psforever.objects.geometry._ +import net.psforever.types.Vector3 +import org.specs2.mutable.Specification + +class GeometryTest extends Specification { + "Point3D" should { + "construct (1)" in { + Point3D(1,2,3.5f) + ok + } + + "construct (2)" in { + Point3D() mustEqual Point3D(0,0,0) + } + + "construct (3)" in { + Point3D(Vector3(1,2,3)) mustEqual Point3D(1,2,3) + } + + "be its own center point" in { + val obj = Point3D(1,2,3.5f) + obj.center mustEqual obj + } + + "define its own exterior" in { + val obj = Point3D(1,2,3.5f) + obj.pointOnOutside(Vector3(1,0,0)) mustEqual obj + obj.pointOnOutside(Vector3(0,1,0)) mustEqual obj + obj.pointOnOutside(Vector3(0,0,1)) mustEqual obj + } + + "convert to Vector3" in { + val obj = Point3D(1,2,3.5f) + obj.asVector3 mustEqual Vector3(1,2,3.5f) + } + } + + "Ray3D" should { + "construct (1)" in { + Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0)) + ok + } + + "construct (2)" in { + Ray3D(1,2,3.5f, Vector3(1,0,0)) mustEqual Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0)) + } + + "construct (3)" in { + Ray3D(Vector3(1,2,3.5f), Vector3(1,0,0)) mustEqual Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0)) + } + + "have a unit vector as its direction vector" in { + Ray3D(1,2,3.5f, Vector3(1,1,1)) must throwA[AssertionError] + } + + "have its target point as the center point" in { + val obj = Ray3D(1,2,3.5f, Vector3(1,0,0)) + obj.center mustEqual Point3D(1,2,3.5f) + } + + "define its own exterior" in { + val obj1 = Ray3D(1,2,3.5f, Vector3(1,0,0)) + val obj2 = Point3D(1,2,3.5f) + obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2 + } + } + + "Line3D" should { + "construct (1)" in { + Line3D(Point3D(1,2,3.5f), Vector3(1,0,0)) + ok + } + + "construct (2)" in { + Line3D(1,2,3.5f, Vector3(1,0,0)) + ok + } + + "construct (3)" in { + Line3D(1,2,3.5f, 2,2,3.5f) mustEqual Line3D(1,2,3.5f, Vector3(1,0,0)) + } + + "have a unit vector as its direction vector" in { + Line3D(1,2,3.5f, Vector3(1,1,1)) must throwA[AssertionError] + } + + "have its target point as the center point" in { + val obj = Line3D(1,2,3.5f, Vector3(1,0,0)) + obj.center mustEqual Point3D(1,2,3.5f) + } + + "define its own exterior" in { + val obj1 = Line3D(1,2,3.5f, Vector3(1,0,0)) + val obj2 = Point3D(1,2,3.5f) + obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2 + } + } + + "Segment3D" should { + "construct (1)" in { + Segment3D(Point3D(1,2,3), Point3D(3,2,3)) + ok + } + + "construct (2)" in { + Segment3D(1,2,3, 3,2,3) mustEqual Segment3D(Point3D(1,2,3), Point3D(3,2,3)) + ok + } + + "construct (3)" in { + Segment3D(Point3D(1,2,3), Vector3(1,0,0)) mustEqual Segment3D(Point3D(1,2,3), Point3D(2,2,3)) + } + + "construct (4)" in { + Segment3D(1,2,3, Vector3(1,0,0)) mustEqual Segment3D(Point3D(1,2,3), Point3D(2,2,3)) + } + + "does not need to have unit vector as its direction vector" in { + val obj1 = Segment3D(1,2,3, Vector3(5,1,1)) + val obj2 = Segment3D(Point3D(1,2,3), Point3D(6,3,4)) + obj1 mustEqual obj2 + obj1.d mustEqual obj2.d + } + + "have a midway point between its two endpoints" in { + Segment3D(Point3D(1,2,3), Point3D(3,4,5)).center mustEqual Point3D(2,3,4) + } + + "report the point on the outside as its center point" in { + val obj1 = Segment3D(Point3D(1,2,3), Point3D(3,4,5)) + val obj2 = obj1.center + obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2 + } + } + + "Sphere3D" should { + "construct (1)" in { + Sphere(Point3D(1,2,3), 3) + ok + } + + "construct (2)" in { + Sphere(3) mustEqual Sphere(Point3D(0,0,0), 3) + ok + } + + "construct (3)" in { + Sphere(1,2,3, 3) mustEqual Sphere(Point3D(1,2,3), 3) + } + + "construct (4)" in { + Sphere(Vector3(1,2,3), 3) mustEqual Sphere(Point3D(1,2,3), 3) + } + + "the center point is self-evident" in { + Sphere(Point3D(1,2,3), 3).center mustEqual Point3D(1,2,3) + } + + "report the point on the outside depending on the requested direction" in { + val obj1 = Sphere(1,2,3, 3) + obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D( 4, 2,3) //east + obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D( 1, 5,3) //north + obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D( 1, 2,6) //up + obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(-2, 2,3) //west + obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D( 1,-1,3) //south + obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D( 1, 2,0) //down + } + } + + "Cylinder (normal)" should { + "construct (1)" in { + Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + ok + } + + "construct (2)" in { + Cylinder(Point3D(1,2,3), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + } + + "construct (3)" in { + Cylinder(Vector3(1,2,3), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + } + + "construct (4)" in { + Cylinder(Vector3(1,2,3), Vector3(0,0,1), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + } + + "report the center point as the center of the cylinder" in { + Cylinder(Point3D(1,2,3), 2, 3).center mustEqual Point3D(1,2,4.5f) + } + + "the point on the outside is different depending on the requested direction" in { + val obj1 = Cylinder(Point3D(1,2,3), 2, 3) + obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D( 3, 2, 4.5f) //east + obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D( 1, 4, 4.5f) //north + obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D( 1, 2, 6f) //up + obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(-1, 2, 4.5f) //west + obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D( 1, 0, 4.5f) //south + obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D( 1, 2, 3f) //down + } + } + + "Cylinder (side tilt)" should { + "not require a specific direction to be relative up" in { + Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3) + ok + } + + "require its specific relative up direction to be expressed as a unit vector" in { + Cylinder(Point3D(1,2,3), Vector3(4,0,0), 2, 3) must throwA[AssertionError] + } + + "report the center point as the center of the cylinder, as if rotated about its base" in { + Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3).center mustEqual Point3D(2.5f, 2, 3) + } + + "report the point on the outside as different depending on the requested direction and the relative up direction" in { + val obj1 = Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3) + obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D(4, 2, 3) //east + obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D(2.5f, 4, 3) //north + obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D(2.5f, 2, 5) //up + obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(1, 2, 3) //west + obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D(2.5f, 0, 3) //south + obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D(2.5f, 2, 1) //down + + val obj2 = Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + obj1.pointOnOutside(Vector3( 1, 0, 0)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 0, 0)) + obj1.pointOnOutside(Vector3( 0, 1, 0)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 1, 0)) + obj1.pointOnOutside(Vector3( 0, 0, 1)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 0, 1)) + obj1.pointOnOutside(Vector3(-1, 0, 0)) mustNotEqual obj2.pointOnOutside(Vector3(-1, 0, 0)) + obj1.pointOnOutside(Vector3( 0,-1, 0)) mustNotEqual obj2.pointOnOutside(Vector3( 0,-1, 0)) + obj1.pointOnOutside(Vector3( 0, 0,-1)) mustNotEqual obj2.pointOnOutside(Vector3( 0, 0,-1)) + } + } +} diff --git a/src/test/scala/objects/IFFLockTest.scala b/src/test/scala/objects/IFFLockTest.scala index b40779d2..a5c008db 100644 --- a/src/test/scala/objects/IFFLockTest.scala +++ b/src/test/scala/objects/IFFLockTest.scala @@ -98,6 +98,6 @@ object IFFLockControlTest { GlobalDefinitions.building ) lock.Owner.Faction = faction - (Player(Avatar(0, "test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), lock) + (Player(Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute)), lock) } } diff --git a/src/test/scala/objects/LoadoutTest.scala b/src/test/scala/objects/LoadoutTest.scala index 426741ac..e2cc2142 100644 --- a/src/test/scala/objects/LoadoutTest.scala +++ b/src/test/scala/objects/LoadoutTest.scala @@ -3,13 +3,13 @@ package objects import net.psforever.objects._ import net.psforever.objects.loadouts._ -import net.psforever.types.{CharacterGender, CharacterVoice, ExoSuitType, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, ExoSuitType, PlanetSideEmpire} import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.avatar.Avatar import org.specs2.mutable._ class LoadoutTest extends Specification { - val avatar = Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1) + val avatar = Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterSex.Female, 41, CharacterVoice.Voice1) def CreatePlayer(): Player = { new Player(avatar) { diff --git a/src/test/scala/objects/MountableTest.scala b/src/test/scala/objects/MountableTest.scala index 5b722161..2dc7a45e 100644 --- a/src/test/scala/objects/MountableTest.scala +++ b/src/test/scala/objects/MountableTest.scala @@ -5,11 +5,10 @@ import akka.actor.{Actor, ActorRef, Props} import base.ActorTest import net.psforever.objects.Player import net.psforever.objects.avatar.Avatar -import net.psforever.objects.definition.{ObjectDefinition, SeatDefinition} -import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.mount._ import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.vehicles.Seat -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, PlanetSideGUID} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, PlanetSideGUID} import scala.concurrent.duration.Duration @@ -26,7 +25,7 @@ class MountableControl1Test extends ActorTest { class MountableControl2Test extends ActorTest { "MountableControl" should { "let a player mount" in { - val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj = new MountableTest.MountableTestObject obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable") val msg = Mountable.TryMount(player, 0) @@ -39,7 +38,7 @@ class MountableControl2Test extends ActorTest { assert(reply2.response.isInstanceOf[Mountable.CanMount]) val reply3 = reply2.response.asInstanceOf[Mountable.CanMount] assert(reply3.obj == obj) - assert(reply3.seat_num == 0) + assert(reply3.seat_number == 0) } } } @@ -47,8 +46,8 @@ class MountableControl2Test extends ActorTest { class MountableControl3Test extends ActorTest { "MountableControl" should { "block a player from mounting" in { - val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) - val player2 = Player(Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + val player2 = Player(Avatar(1, "test2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj = new MountableTest.MountableTestObject obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable") obj.Actor ! Mountable.TryMount(player1, 0) @@ -62,35 +61,25 @@ class MountableControl3Test extends ActorTest { assert(reply2.response.isInstanceOf[Mountable.CanNotMount]) val reply3 = reply2.response.asInstanceOf[Mountable.CanNotMount] assert(reply3.obj == obj) - assert(reply3.seat_num == 0) + assert(reply3.mount_point == 0) } } } object MountableTest { class MountableTestObject extends PlanetSideServerObject with Mountable { - private val seats: Map[Int, Seat] = Map(0 -> new Seat(new SeatDefinition())) - def Seats: Map[Int, Seat] = seats - def Seat(seatNum: Int): Option[Seat] = seats.get(seatNum) - def MountPoints: Map[Int, Int] = Map(1 -> 0) - def GetSeatFromMountPoint(mount: Int): Option[Int] = MountPoints.get(mount) - def PassengerInSeat(user: Player): Option[Int] = { - if (seats(0).Occupant.contains(user)) { - Some(0) - } else { - None - } - } + seats += 0 -> new Seat(new SeatDefinition()) GUID = PlanetSideGUID(1) //eh whatever def Faction = PlanetSideEmpire.TR - def Definition: ObjectDefinition = null + def Definition = new ObjectDefinition(1) with MountableDefinition { + MountPoints += 0 -> MountInfo(0) + } } class MountableTestControl(obj: PlanetSideServerObject with Mountable) extends Actor - with MountableBehavior.Mount - with MountableBehavior.Dismount { + with MountableBehavior { override def MountableObject = obj def receive: Receive = mountBehavior.orElse(dismountBehavior) diff --git a/src/test/scala/objects/OrbitalShuttlePadTest.scala b/src/test/scala/objects/OrbitalShuttlePadTest.scala new file mode 100644 index 00000000..cce0ba30 --- /dev/null +++ b/src/test/scala/objects/OrbitalShuttlePadTest.scala @@ -0,0 +1,100 @@ +// Copyright (c) 2021 PSForever +package objects + +import akka.actor.{ActorRef, Props} +import akka.routing.RandomPool +import akka.testkit.TestProbe +import base.FreedContextActorTest +import net.psforever.actors.zone.BuildingActor +import net.psforever.objects.guid.actor.UniqueNumberSystem +import net.psforever.objects.{GlobalDefinitions, Vehicle} +import net.psforever.objects.guid.{NumberPoolHub, TaskResolver} +import net.psforever.objects.guid.source.MaxNumberSource +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.shuttle.{OrbitalShuttle, OrbitalShuttlePad, OrbitalShuttlePadControl, ShuttleAmenity} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.zones.{Zone, ZoneMap, ZoneVehicleActor} +import net.psforever.services.{InterstellarClusterService, Service, ServiceManager} +import net.psforever.services.galaxy.GalaxyService +import net.psforever.services.hart.HartService +import net.psforever.types.PlanetSideEmpire + +import scala.collection.concurrent.TrieMap +import scala.collection.mutable.ListBuffer +import scala.concurrent.duration._ + +class OrbitalShuttlePadControltest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + val services = ServiceManager.boot(system) + services ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + services ! ServiceManager.Register(Props[HartService](), "hart") + expectNoMessage(1000 milliseconds) + var buildingMap = new TrieMap[Int, Building]() + val vehicles = ListBuffer[Vehicle]() + val guid = new NumberPoolHub(new MaxNumberSource(max = 15)) + guid.AddPool("dynamic", (11 to 15).toList) + val catchall = new TestProbe(system).ref + val resolver = context.actorOf(RandomPool(1).props(Props[TaskResolver]()), s"test-taskResolver") + val uns = context.actorOf( + RandomPool(1).props( + Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(this.guid)) + ), + s"test-uns" + ) + val zone = new Zone("test", new ZoneMap("test-map"), 0) { + val transport: ActorRef = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"zone-test-vehicles") + + override def SetupNumberPools() = {} + GUID(guid) + override def GUID = { uns } + override def AvatarEvents = catchall + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + override def Transport = { transport } + override def Vehicles = { vehicles.toList } + override def Buildings = { buildingMap.toMap } + override def tasks = { resolver } + } + val building = new Building( + name = "test-orbital-building-tr", + building_guid = 1, + map_id = 0, + zone, + StructureType.Building, + GlobalDefinitions.orbital_building_tr + ) + building.Faction = PlanetSideEmpire.TR + buildingMap += 1 -> building + building.Actor = context.spawn(BuildingActor(zone, building), "test-orbital-building-tr-control").toClassic + building.Invalidate() + guid.register(building, number = 1) + + (3 to 10).foreach { index => + val door = Door(GlobalDefinitions.gr_door_mb_orb) + building.Amenities = door + door.Actor = catchall + guid.register(door, index) + } + + val pad = new OrbitalShuttlePad(GlobalDefinitions.obbasemesh) + guid.register(pad, number = 2) + pad.Actor = system.actorOf(Props(classOf[OrbitalShuttlePadControl], pad), "test-shuttle-pad") + building.Amenities = pad + + "OrbitalShuttlePad" should { + "startup and create the shuttle" in { + assert(building.Amenities.size == 9) + assert(vehicles.isEmpty) + pad.Actor ! Service.Startup() + expectNoMessage(max = 5 seconds) + assert(building.Amenities.size == 10) + assert(vehicles.size == 1) + assert(building.Amenities(9).isInstanceOf[ShuttleAmenity]) //the shuttle is an amenity of the building now + assert(vehicles.head.isInstanceOf[OrbitalShuttle]) //here is the shuttle + } + } +} + +object OrbitalShuttlePadTest { /* intentionally blank */ } diff --git a/src/test/scala/objects/OrbitalShuttleTest.scala b/src/test/scala/objects/OrbitalShuttleTest.scala new file mode 100644 index 00000000..9493280e --- /dev/null +++ b/src/test/scala/objects/OrbitalShuttleTest.scala @@ -0,0 +1,274 @@ +// Copyright (c) 2021 PSForever +package objects + +import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.avatar.Avatar +import net.psforever.objects.serverobject.shuttle.OrbitalShuttle +import net.psforever.objects.vehicles.AccessPermissionGroup +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire} +import org.specs2.mutable.Specification + +class OrbitalShuttleTest extends Specification { + val testAvatar1 = Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) + val testAvatar2 = Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) + val testAvatar3 = Avatar(2, "TestCharacter3", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) + + "OrbitalShuttle" should { + "construct (proper definition)" in { + new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + ok + } + + "construct (any type of vehicle)" in { + new OrbitalShuttle(GlobalDefinitions.fury) + ok + } + + "only use known mount points" in { + val fury = new OrbitalShuttle(GlobalDefinitions.fury) + fury.MountPoints.get(0).isEmpty mustEqual true + fury.MountPoints.get(1).nonEmpty mustEqual true + fury.MountPoints.get(2).nonEmpty mustEqual true + fury.MountPoints.get(3).isEmpty mustEqual true + + val shuttle = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + shuttle.MountPoints.get(0).isEmpty mustEqual true + shuttle.MountPoints.get(1).nonEmpty mustEqual true + shuttle.MountPoints.get(2).nonEmpty mustEqual true + shuttle.MountPoints.get(3).nonEmpty mustEqual true + shuttle.MountPoints.get(4).nonEmpty mustEqual true + shuttle.MountPoints.get(5).nonEmpty mustEqual true + shuttle.MountPoints.get(6).nonEmpty mustEqual true + shuttle.MountPoints.get(7).nonEmpty mustEqual true + shuttle.MountPoints.get(8).nonEmpty mustEqual true + shuttle.MountPoints.get(9).isEmpty mustEqual true + } + + "will only discover unoccupied seats" in { + val fury1 = new OrbitalShuttle(GlobalDefinitions.fury) + val player1 = Player(testAvatar1) + fury1.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury1.Seat(seatNumber) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + case _ => ko + } + fury1.GetSeatFromMountPoint(mountPoint = 1).isEmpty mustEqual true //seat is occupied + + //comparison with normal Vehicle + val fury2 = new Vehicle(GlobalDefinitions.fury) + val player2 = Player(testAvatar2) + fury2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + case _ => ko + } + fury2.GetSeatFromMountPoint(mountPoint = 1).contains(0) mustEqual true //even though seat is occupied + } + + "have a fixed number of normal seats (using normal definition)" in { + val fury1 = new OrbitalShuttle(GlobalDefinitions.fury) + fury1.Seats.size mustEqual 1 + fury1.MountPoints.size mustEqual 2 + val player1 = Player(testAvatar1) + fury1.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury1.Seat(seatNumber) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + case _ => ko + } + fury1.Seats.size mustEqual 1 + fury1.MountPoints.size mustEqual 2 + val player2 = Player(testAvatar2) + fury1.GetSeatFromMountPoint(mountPoint = 1).isEmpty mustEqual true + fury1.Seats.size mustEqual 1 + fury1.MountPoints.size mustEqual 2 + + //congruent with normal Vehicle + val fury2 = new Vehicle(GlobalDefinitions.fury) + fury2.Seats.size mustEqual 1 + fury2.MountPoints.size mustEqual 2 + val player3 = Player(testAvatar3) + fury2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player3).contains(player3) mustEqual true + case _ => ko + } + case _ => ko + } + fury2.Seats.size mustEqual 1 + fury2.MountPoints.size mustEqual 2 + fury2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => fury2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual false + case _ => ko + } + case _ => ko + } + fury2.Seats.size mustEqual 1 + fury2.MountPoints.size mustEqual 2 + } + + "create seats as needed (with appropriate definition)" in { + GlobalDefinitions.fury + .Seats(0).occupancy == 1 mustEqual true + GlobalDefinitions.orbital_shuttle + .Seats(0).occupancy > 1 mustEqual true + + val shuttle1 = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + shuttle1.Seats.size mustEqual 1 + shuttle1.MountPoints.size mustEqual 8 + val player1 = Player(testAvatar1) + shuttle1.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => shuttle1.Seat(seatNumber) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + case _ => ko + } + shuttle1.Seats.size mustEqual 1 + shuttle1.MountPoints.size mustEqual 8 + val player2 = Player(testAvatar2) + shuttle1.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => shuttle1.Seat(seatNumber) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + case _ => ko + } + shuttle1.Seats.size mustEqual 2 + shuttle1.MountPoints.size mustEqual 8 + + //comparison with normal Vehicle + val shuttle2 = new Vehicle(GlobalDefinitions.orbital_shuttle) + shuttle2.Seats.size mustEqual 1 + shuttle2.MountPoints.size mustEqual 8 + val player3 = Player(testAvatar3) + shuttle2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => shuttle2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player3).contains(player3) mustEqual true + case _ => ko + } + case _ => ko + } + shuttle2.Seats.size mustEqual 1 + shuttle2.MountPoints.size mustEqual 8 + shuttle2.GetSeatFromMountPoint(mountPoint = 1) match { + case Some(seatNumber) => shuttle2.Seat(seatNumber) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual false + case _ => ko + } + case _ => ko + } + shuttle2.Seats.size mustEqual 1 + shuttle2.MountPoints.size mustEqual 8 + } + + "not create new seats out of order" in { + val shuttle = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + val player1 = Player(testAvatar1) + shuttle.Seat(seatNumber = 0) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + val player2 = Player(testAvatar2) + shuttle.Seat(seatNumber = 2).isEmpty mustEqual true + } + + "recognize proper seating arrangements" in { + val shuttle = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + val player1 = Player(testAvatar1) + shuttle.Seat(seatNumber = 0) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + val player2 = Player(testAvatar2) + shuttle.Seat(seatNumber = 1) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + val player3 = Player(testAvatar3) + shuttle.Seat(seatNumber = 2) match { + case Some(seat) => seat.mount(player3).contains(player3) mustEqual true + case _ => ko + } + shuttle.PassengerInSeat(player1).contains(0) mustEqual true + shuttle.PassengerInSeat(player2).contains(1) mustEqual true + shuttle.PassengerInSeat(player3).contains(2) mustEqual true + } + + "retain created seats after dismount" in { + val shuttle = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + val player1 = Player(testAvatar1) + shuttle.Seat(seatNumber = 0) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + val player2 = Player(testAvatar2) + shuttle.Seat(seatNumber = 1) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + val player3 = Player(testAvatar3) + shuttle.Seat(seatNumber = 2) match { + case Some(seat) => seat.mount(player3).contains(player3) mustEqual true + case _ => ko + } + + shuttle.Seats(0).isOccupied mustEqual true + shuttle.Seats(1).isOccupied mustEqual true + shuttle.Seats(2).isOccupied mustEqual true + shuttle.Seats.size mustEqual 3 + //IMPORTANT TO NOTE + shuttle.GetSeatFromMountPoint(mountPoint = 1).contains(3) mustEqual true //new seat + + shuttle.Seat(seatNumber = 1) match { + case Some(seat) => seat.unmount(player2).isEmpty mustEqual true + case _ => ko + } + shuttle.Seats(0).isOccupied mustEqual true + shuttle.Seats(1).isOccupied mustEqual false + shuttle.Seats(2).isOccupied mustEqual true + shuttle.Seats.size mustEqual 3 + //IMPORTANT TO NOTE + shuttle.GetSeatFromMountPoint(mountPoint = 1).contains(1) mustEqual true //reuse newly unoccupied seat + } + + "consider all seats as passenger seats" in { + val fury1 = Vehicle(GlobalDefinitions.fury) + fury1.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Driver) + fury1.SeatPermissionGroup(seatNumber = 1).isEmpty mustEqual true + + val fury2 = Vehicle(GlobalDefinitions.orbital_shuttle) + fury2.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Driver) + fury2.SeatPermissionGroup(seatNumber = 1).isEmpty mustEqual true + + val shuttle1 = new OrbitalShuttle(GlobalDefinitions.fury) + shuttle1.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Passenger) + shuttle1.SeatPermissionGroup(seatNumber = 1).isEmpty mustEqual true + + val shuttle2 = new OrbitalShuttle(GlobalDefinitions.orbital_shuttle) + shuttle2.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Passenger) + shuttle2.SeatPermissionGroup(seatNumber = 1).contains(AccessPermissionGroup.Passenger) //seat does not exist yet + shuttle2.SeatPermissionGroup(seatNumber = 2).isEmpty mustEqual true + val player1 = Player(testAvatar1) + shuttle2.Seat(seatNumber = 0) match { + case Some(seat) => seat.mount(player1).contains(player1) mustEqual true + case _ => ko + } + val player2 = Player(testAvatar2) + shuttle2.Seat(seatNumber = 1) match { + case Some(seat) => seat.mount(player2).contains(player2) mustEqual true + case _ => ko + } + shuttle2.SeatPermissionGroup(seatNumber = 0).contains(AccessPermissionGroup.Passenger) + shuttle2.SeatPermissionGroup(seatNumber = 1).contains(AccessPermissionGroup.Passenger) + shuttle2.SeatPermissionGroup(seatNumber = 2).contains(AccessPermissionGroup.Passenger) //seat does not exist yet + shuttle2.SeatPermissionGroup(seatNumber = 3).isEmpty mustEqual true + } + } +} diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index db0b5d46..4f0d237e 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -26,9 +26,9 @@ import scala.concurrent.duration._ class PlayerControlHealTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val player2 = - Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=2 val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { @@ -111,7 +111,7 @@ class PlayerControlHealTest extends ActorTest { } class PlayerControlHealSelfTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { @@ -186,9 +186,9 @@ class PlayerControlHealSelfTest extends ActorTest { class PlayerControlRepairTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val player2 = - Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=2 val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { @@ -282,7 +282,7 @@ class PlayerControlRepairTest extends ActorTest { class PlayerControlRepairSelfTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { @@ -357,9 +357,9 @@ class PlayerControlRepairSelfTest extends ActorTest { class PlayerControlDamageTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val player2 = - Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=2 val avatarProbe = TestProbe() val activityProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) @@ -462,9 +462,9 @@ class PlayerControlDamageTest extends ActorTest { class PlayerControlDeathStandingTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val player2 = - Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=2 val avatarProbe = TestProbe() val activityProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) @@ -606,9 +606,9 @@ class PlayerControlDeathStandingTest extends ActorTest { class PlayerControlDeathSeatedTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val player2 = - Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=2 val avatarProbe = TestProbe() val activityProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) @@ -666,7 +666,7 @@ class PlayerControlDeathSeatedTest extends ActorTest { "handle death when seated (in something)" in { player2.Health = player2.Definition.DamageDestroysAt + 1 //initial state manip player2.VehicleSeated = vehicle.GUID //initial state manip, anything - vehicle.Seats(0).Occupant = player2 + vehicle.Seats(0).mount(player2) player2.Armor = 0 //initial state manip assert(player2.Health > player2.Definition.DamageDestroysAt) assert(player2.isAlive) @@ -768,7 +768,7 @@ class PlayerControlDeathSeatedTest extends ActorTest { class PlayerControlInteractWithWaterTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) @@ -820,7 +820,7 @@ class PlayerControlInteractWithWaterTest extends ActorTest { class PlayerControlStopInteractWithWaterTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) @@ -883,7 +883,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest { class PlayerControlInteractWithLavaTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val pool = Pool(EnvironmentAttribute.Lava, DeepSquare(-1, 10, 10, 0, 0)) @@ -943,7 +943,7 @@ class PlayerControlInteractWithLavaTest extends ActorTest { class PlayerControlInteractWithDeathTest extends ActorTest { val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val pool = Pool(EnvironmentAttribute.Death, DeepSquare(-1, 10, 10, 0, 0)) diff --git a/src/test/scala/objects/PlayerTest.scala b/src/test/scala/objects/PlayerTest.scala index d8f91da8..4b645bf0 100644 --- a/src/test/scala/objects/PlayerTest.scala +++ b/src/test/scala/objects/PlayerTest.scala @@ -6,6 +6,7 @@ import net.psforever.objects._ import net.psforever.objects.avatar.Avatar import net.psforever.objects.definition.{SimpleItemDefinition, SpecialExoSuitDefinition} import net.psforever.objects.equipment.EquipmentSize +import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.locker.LockerEquipment import net.psforever.types.{PlanetSideGUID, _} import org.specs2.mutable._ @@ -16,7 +17,7 @@ class PlayerTest extends Specification { def TestPlayer( name: String, faction: PlanetSideEmpire.Value, - sex: CharacterGender.Value, + sex: CharacterSex, head: Int, voice: CharacterVoice.Value ): Player = { @@ -25,7 +26,7 @@ class PlayerTest extends Specification { "Player" should { "construct" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.isAlive mustEqual false obj.FacingYawUpper mustEqual 0 obj.Jumping mustEqual false @@ -43,7 +44,7 @@ class PlayerTest extends Specification { } "(re)spawn" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.isAlive mustEqual false obj.Health mustEqual 0 obj.Armor mustEqual 0 @@ -56,7 +57,7 @@ class PlayerTest extends Specification { } "will not (re)spawn if not dead" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Spawn() obj.Health mustEqual 100 obj.Armor mustEqual 50 @@ -72,7 +73,7 @@ class PlayerTest extends Specification { } "can die" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Spawn() obj.Armor = 35 //50 -> 35 obj.isAlive mustEqual true @@ -85,7 +86,7 @@ class PlayerTest extends Specification { } "can not become a backpack if alive" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Spawn() obj.isAlive mustEqual true obj.isBackpack mustEqual false @@ -95,7 +96,7 @@ class PlayerTest extends Specification { } "can become a backpack" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.isAlive mustEqual false obj.isBackpack mustEqual false obj.Release @@ -104,7 +105,7 @@ class PlayerTest extends Specification { } "set new maximum values (health, stamina)" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.MaxHealth mustEqual 100 obj.MaxHealth = 123 obj.MaxHealth mustEqual 123 @@ -114,7 +115,7 @@ class PlayerTest extends Specification { } // "set new values (health, armor, stamina) but only when alive" in { -// val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) +// val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) // obj.Health = 23 // obj.Armor = 34 // obj.Stamina = 45 @@ -135,7 +136,7 @@ class PlayerTest extends Specification { // } "has visible slots" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.VisibleSlots mustEqual Set(0, 2, 4) //Standard obj.ExoSuit = ExoSuitType.Agile obj.VisibleSlots mustEqual Set(0, 1, 2, 4) @@ -148,7 +149,7 @@ class PlayerTest extends Specification { } "init (Standard Exo-Suit)" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.ExoSuit mustEqual ExoSuitType.Standard obj.Slot(0).Size mustEqual EquipmentSize.Pistol obj.Slot(1).Size mustEqual EquipmentSize.Blocked @@ -162,7 +163,7 @@ class PlayerTest extends Specification { "draw equipped holsters only" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Slot(1).Size = EquipmentSize.Pistol obj.Slot(1).Equipment = wep obj.DrawnSlot mustEqual Player.HandsDownSlot @@ -175,7 +176,7 @@ class PlayerTest extends Specification { "remember the last drawn holster" in { val wep1 = SimpleItem(SimpleItemDefinition(149)) val wep2 = SimpleItem(SimpleItemDefinition(149)) - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Slot(0).Size = EquipmentSize.Pistol obj.Slot(0).Equipment = wep1 obj.Slot(1).Size = EquipmentSize.Pistol @@ -214,7 +215,7 @@ class PlayerTest extends Specification { "hold something in their free hand" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Slot(Player.FreeHandSlot).Equipment = wep obj.Slot(Player.FreeHandSlot).Equipment.get.Definition.ObjectId mustEqual 149 @@ -222,14 +223,14 @@ class PlayerTest extends Specification { "provide an invalid hand that can not hold anything" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Slot(-1).Equipment = wep obj.Slot(-1).Equipment.isEmpty mustEqual true } "search for the smallest available slot in which to store equipment" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Inventory.Resize(3, 3) //fits one item obj.Fit(Tool(GlobalDefinitions.beamer)).contains(0) mustEqual true @@ -247,7 +248,7 @@ class PlayerTest extends Specification { } "can use their free hand to hold things" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val ammo = AmmoBox(GlobalDefinitions.bullet_9mm) obj.FreeHand.Equipment.isEmpty mustEqual true @@ -256,12 +257,12 @@ class PlayerTest extends Specification { } "can access the player's locker-space" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Slot(5).Equipment.get.isInstanceOf[LockerEquipment] mustEqual true } "can find equipment" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Slot(0).Equipment = { val item = Tool(beamer) item.GUID = PlanetSideGUID(1) @@ -297,7 +298,7 @@ class PlayerTest extends Specification { } "does equipment collision checking (are we already holding something there?)" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val item1 = Tool(beamer) val item2 = Kit(medkit) val item3 = AmmoBox(GlobalDefinitions.bullet_9mm) @@ -306,7 +307,7 @@ class PlayerTest extends Specification { obj.FreeHand.Equipment = item3 obj.Collisions(0, 1, 1) match { - case Success(List(item)) => + case Success(List(item: InventoryItem)) => item.obj mustEqual item1 item.start mustEqual 0 case _ => @@ -320,7 +321,7 @@ class PlayerTest extends Specification { } //holsters, nothing obj.Collisions(6, 1, 1) match { - case Success(List(item)) => + case Success(List(item: InventoryItem)) => item.obj mustEqual item2 item.start mustEqual 6 case _ => @@ -328,16 +329,17 @@ class PlayerTest extends Specification { } //inventory obj.Collisions(Player.FreeHandSlot, 1, 1) match { - case Success(List(item)) => + case Success(List(item: InventoryItem)) => item.obj mustEqual item3 item.start mustEqual Player.FreeHandSlot case _ => ko } //free hand + ok } - "seat in a vehicle" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + "mount in a vehicle" in { + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.VehicleSeated.isEmpty mustEqual true obj.VehicleSeated = PlanetSideGUID(65) obj.VehicleSeated.contains(PlanetSideGUID(65)) mustEqual true @@ -346,7 +348,7 @@ class PlayerTest extends Specification { } "own in a vehicle" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.avatar.vehicle.isEmpty mustEqual true obj.avatar.vehicle = Some(PlanetSideGUID(65)) obj.avatar.vehicle.contains(PlanetSideGUID(65)) mustEqual true @@ -355,21 +357,21 @@ class PlayerTest extends Specification { } "remember what zone he is in" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.Continent mustEqual "home2" obj.Continent = "ugd01" obj.Continent mustEqual "ugd01" } "special is typically normal and can not be changed from normal" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal obj.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal } "a TR MAX can change its special to Overdrive or Anchored" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.ExoSuit = ExoSuitType.MAX obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal obj.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored @@ -384,7 +386,7 @@ class PlayerTest extends Specification { } "an NC MAX can change its special to Shielded" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.ExoSuit = ExoSuitType.MAX obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal obj.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded @@ -394,13 +396,13 @@ class PlayerTest extends Specification { } "one faction can not use the other's specials" in { - val objtr = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val objtr = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) objtr.ExoSuit = ExoSuitType.MAX objtr.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal objtr.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded objtr.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal - val objnc = TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Voice5) + val objnc = TestPlayer("Chord", PlanetSideEmpire.NC, CharacterSex.Male, 0, CharacterVoice.Voice5) objnc.ExoSuit = ExoSuitType.MAX objnc.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal objnc.UsingSpecial = SpecialExoSuitDefinition.Mode.Overdrive @@ -410,7 +412,7 @@ class PlayerTest extends Specification { } "changing exo-suit type resets the special to Normal (and changing back does not revert it again)" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) obj.ExoSuit = ExoSuitType.MAX obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal obj.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored diff --git a/src/test/scala/objects/ProjectileTest.scala b/src/test/scala/objects/ProjectileTest.scala index 418a2883..cf57cbbe 100644 --- a/src/test/scala/objects/ProjectileTest.scala +++ b/src/test/scala/objects/ProjectileTest.scala @@ -13,7 +13,7 @@ import net.psforever.types.{PlanetSideGUID, _} import org.specs2.mutable.Specification class ProjectileTest extends Specification { - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val fury = Vehicle(GlobalDefinitions.fury) "Range" should { @@ -209,7 +209,7 @@ class ProjectileTest extends Specification { "contain timely information" in { val obj = - Player(Avatar(0, "TestCharacter-alt", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + Player(Avatar(0, "TestCharacter-alt", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) obj.VehicleSeated = Some(PlanetSideGUID(1)) obj.Position = Vector3(1.2f, 3.4f, 5.6f) obj.Orientation = Vector3(2.1f, 4.3f, 6.5f) @@ -333,7 +333,7 @@ class ProjectileTest extends Specification { "Projectile DamageInteraction" should { //TODO wrong place for this test? val beamer_wep = Tool(GlobalDefinitions.beamer) val p_source = PlayerSource(player) - val player2 = Player(Avatar(0, "TestTarget", PlanetSideEmpire.NC, CharacterGender.Female, 1, CharacterVoice.Mute)) + val player2 = Player(Avatar(0, "TestTarget", PlanetSideEmpire.NC, CharacterSex.Female, 1, CharacterVoice.Mute)) val p2_source = PlayerSource(player2) val projectile = Projectile( beamer_wep.Projectile, diff --git a/src/test/scala/objects/RepairableTest.scala b/src/test/scala/objects/RepairableTest.scala index 70750d44..6adf2f99 100644 --- a/src/test/scala/objects/RepairableTest.scala +++ b/src/test/scala/objects/RepairableTest.scala @@ -37,7 +37,7 @@ class RepairableEntityRepairTest extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val gen = Generator(GlobalDefinitions.generator) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() guid.register(building, 1) guid.register(gen, 2) @@ -109,7 +109,7 @@ class RepairableEntityNotRepairTest extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val gen = Generator(GlobalDefinitions.generator) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() guid.register(building, 1) guid.register(gen, 2) @@ -150,7 +150,7 @@ class RepairableAmenityTest extends ActorTest { val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 val term = Terminal(GlobalDefinitions.order_terminal) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() guid.register(building, 1) guid.register(term, 2) @@ -251,7 +251,7 @@ class RepairableTurretWeapon extends ActorTest { val turretWeapon = turret.Weapons.values.head.Equipment.get.asInstanceOf[Tool] val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=3 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player1Probe = TestProbe() @@ -337,7 +337,7 @@ class RepairableVehicleRepair extends ActorTest { val atvWeapon = atv.Weapons(1).Equipment.get.asInstanceOf[Tool] val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player1Probe = TestProbe() @@ -410,7 +410,7 @@ class RepairableVehicleRestoration extends ActorTest { val atvWeapon = atv.Weapons(1).Equipment.get.asInstanceOf[Tool] val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=4 player1.Spawn() player1.Position = Vector3(2, 2, 2) val player1Probe = TestProbe() diff --git a/src/test/scala/objects/ResourceSiloTest.scala b/src/test/scala/objects/ResourceSiloTest.scala index f0cd6c83..612b5d1b 100644 --- a/src/test/scala/objects/ResourceSiloTest.scala +++ b/src/test/scala/objects/ResourceSiloTest.scala @@ -3,8 +3,8 @@ package objects import akka.actor.{Actor, Props} import akka.testkit.TestProbe -import base.ActorTest -import net.psforever.actors.zone.{BuildingActor, ZoneActor} +import base.{ActorTest, FreedContextActorTest} +import net.psforever.actors.zone.BuildingActor import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.CommonMessages @@ -17,9 +17,11 @@ import net.psforever.packet.game.UseItemMessage import net.psforever.types._ import org.specs2.mutable.Specification import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import akka.actor.typed.scaladsl.adapter._ import net.psforever.objects.avatar.Avatar +import net.psforever.services.{InterstellarClusterService, ServiceManager} +import net.psforever.services.galaxy.GalaxyService +import scala.collection.concurrent.TrieMap import scala.concurrent.duration._ class ResourceSiloTest extends Specification { @@ -145,51 +147,60 @@ class ResourceSiloControlStartupMessageSomeTest extends ActorTest { } } -class ResourceSiloControlUseTest extends ActorTest { - val guid = new NumberPoolHub(new MaxNumberSource(10)) - val map = new ZoneMap("test") - val zone = new Zone("test", map, 0) { +class ResourceSiloControlUseTest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + expectNoMessage(1000 milliseconds) + var buildingMap = new TrieMap[Int, Building]() + val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + val ant = Vehicle(GlobalDefinitions.ant) + val silo = new ResourceSilo() + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test-map"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def AvatarEvents = catchall + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + override def Vehicles = List(ant) + override def Buildings = { buildingMap.toMap } } - zone.actor = system.spawnAnonymous(ZoneActor(zone)) val building = new Building( - "Building", - building_guid = 0, + name = "integ-fac-test-building", + building_guid = 6, map_id = 0, zone, - StructureType.Building, - GlobalDefinitions.building - ) //guid=1 - building.Actor = TestProbe("building-actor").ref + StructureType.Facility, + GlobalDefinitions.cryo_facility + ) + buildingMap += 6 -> building + building.Actor = context.spawn(BuildingActor(zone, building), "integ-fac-test-building-control").toClassic + building.Invalidate() - val obj = ResourceSilo() //guid=2 - obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") - obj.Owner = building - obj.Actor ! "startup" + guid.register(player, number = 1) + guid.register(ant, number = 2) + guid.register(silo, number = 5) + guid.register(building, number = 6) - val player = Player( - new Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) - ) //guid=3 - val vehicle = Vehicle(GlobalDefinitions.ant) //guid=4 - val probe = new TestProbe(system) - - guid.register(building, 1) - guid.register(obj, 2) - guid.register(player, 3) - guid.register(vehicle, 4) - expectNoMessage(200 milliseconds) - zone.Transport ! Zone.Vehicle.Spawn(vehicle) - vehicle.Seats(0).Occupant = player - player.VehicleSeated = vehicle.GUID - expectNoMessage(200 milliseconds) - system.stop(vehicle.Actor) - vehicle.Actor = probe.ref + val maxNtuCap = ant.Definition.MaxNtuCapacitor + player.Spawn() + ant.NtuCapacitor = maxNtuCap + val probe = new TestProbe(system) + ant.Actor = probe.ref + ant.Zone = zone + ant.Seats(0).mount(player) + ant.DeploymentState = DriveState.Deployed + building.Amenities = silo + silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") + silo.Actor ! "startup" "Resource silo" should { "respond when being used" in { expectNoMessage(1 seconds) - obj.Actor ! CommonMessages.Use(ResourceSiloTest.player) + silo.Actor ! CommonMessages.Use(ResourceSiloTest.player) val reply = probe.receiveOne(2000 milliseconds) assert(reply match { @@ -416,7 +427,7 @@ class ResourceSiloControlNoUpdateTest extends ActorTest { object ResourceSiloTest { val player = Player( - new Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + new Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) ) class ProbedAvatarService(probe: TestProbe) extends Actor { diff --git a/src/test/scala/objects/ServerObjectBuilderTest.scala b/src/test/scala/objects/ServerObjectBuilderTest.scala index caefd9f9..448633d0 100644 --- a/src/test/scala/objects/ServerObjectBuilderTest.scala +++ b/src/test/scala/objects/ServerObjectBuilderTest.scala @@ -226,6 +226,21 @@ class FacilityTurretObjectBuilderTest extends FreedContextActorTest { } } +class OrbitalShuttlePadObjectBuilderTest extends FreedContextActorTest { + import net.psforever.objects.GlobalDefinitions.obbasemesh + import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad + "OrbitalShuttlePadObjectBuilder" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val obj = ServerObjectBuilder(1, OrbitalShuttlePad.Constructor(Vector3.Zero, obbasemesh, Vector3.Zero)).Build(context, hub) + assert(obj.isInstanceOf[OrbitalShuttlePad]) + assert(obj.HasGUID) + assert(obj.GUID == PlanetSideGUID(1)) + assert(obj == hub(1).get) + } + } +} + object ServerObjectBuilderTest { import net.psforever.objects.guid.source.MaxNumberSource def NumberPoolHub: NumberPoolHub = { diff --git a/src/test/scala/objects/UtilityTest.scala b/src/test/scala/objects/UtilityTest.scala index 31b615df..b342ba8a 100644 --- a/src/test/scala/objects/UtilityTest.scala +++ b/src/test/scala/objects/UtilityTest.scala @@ -60,7 +60,7 @@ class UtilityTest extends Specification { val veh = Vehicle(GlobalDefinitions.quadstealth) veh.Faction = PlanetSideEmpire.TR val obj = Utility(UtilityType.teleportpad_terminal, UtilityTest.vehicle) - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) veh.GUID = PlanetSideGUID(101) obj().Owner = veh //hack obj().GUID = PlanetSideGUID(1) diff --git a/src/test/scala/objects/VehicleControlTest.scala b/src/test/scala/objects/VehicleControlTest.scala index ac3d833a..8a6f6353 100644 --- a/src/test/scala/objects/VehicleControlTest.scala +++ b/src/test/scala/objects/VehicleControlTest.scala @@ -1,7 +1,7 @@ // Copyright (c) 2020 PSForever package objects -import akka.actor.Props +import akka.actor.{ActorRef, Props} import akka.actor.typed.scaladsl.adapter._ import akka.testkit.TestProbe import base.{ActorTest, FreedContextActorTest} @@ -55,7 +55,7 @@ class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { vehicle.GUID = PlanetSideGUID(1) player1.GUID = PlanetSideGUID(2) - vehicle.Seats(1).Occupant = player1 //passenger seat + vehicle.Seats(1).mount(player1) //passenger mount player1.VehicleSeated = vehicle.GUID expectNoMessage(200 milliseconds) @@ -75,7 +75,7 @@ class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { } ) assert(player1.VehicleSeated.isEmpty) - assert(vehicle.Seats(1).Occupant.isEmpty) + assert(vehicle.Seats(1).occupant.isEmpty) } } } @@ -109,17 +109,16 @@ class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTes util().GUID = PlanetSideGUID(utilityId) utilityId += 1 } - vehicle.Seats(1).Occupant = player1 //passenger seat + vehicle.Seats(1).mount(player1) //passenger mount player1.VehicleSeated = vehicle.GUID - lodestar.Seats(0).Occupant = player2 + lodestar.Seats(0).mount(player2) player2.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = vehicle + lodestar.CargoHolds(1).mount(vehicle) vehicle.MountedIn = lodestar.GUID val vehicleProbe = new TestProbe(system) zone.VehicleEvents = vehicleProbe.ref zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this - expectNoMessage(200 milliseconds) "VehicleControl" should { "if mounted as cargo, self-eject when marked for deconstruction" in { @@ -196,7 +195,7 @@ class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTes } ) assert(player1.VehicleSeated.isEmpty) - assert(vehicle.Seats(1).Occupant.isEmpty) + assert(vehicle.Seats(1).occupant.isEmpty) } } } @@ -232,17 +231,16 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor util().GUID = PlanetSideGUID(utilityId) utilityId += 1 } - vehicle.Seats(1).Occupant = player1 //passenger seat + vehicle.Seats(1).mount(player1) //passenger mount player1.VehicleSeated = vehicle.GUID - lodestar.Seats(0).Occupant = player2 + lodestar.Seats(0).mount(player2) player2.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = vehicle + lodestar.CargoHolds(1).mount(vehicle) vehicle.MountedIn = lodestar.GUID val vehicleProbe = new TestProbe(system) zone.VehicleEvents = vehicleProbe.ref zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this - expectNoMessage(200 milliseconds) "VehicleControl" should { "if with mounted cargo, eject it when marked for deconstruction" in { @@ -260,7 +258,7 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor } ) assert(player2.VehicleSeated.isEmpty) - assert(lodestar.Seats(0).Occupant.isEmpty) + assert(lodestar.Seats(0).occupant.isEmpty) //cargo dismounting messages assert( vehicle_msg(1) match { @@ -323,87 +321,73 @@ class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActor } class VehicleControlMountingBlockedExosuitTest extends ActorTest { - val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) - case _ => - assert(false) - } - } - - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } + val catchallProbe = new TestProbe(system) + val catchall = catchallProbe.ref + val zone = new Zone("test", new ZoneMap("test-map"), 0) { + override def SetupNumberPools(): Unit = {} + override def AvatarEvents: ActorRef = catchall + override def LocalEvents: ActorRef = catchall + override def VehicleEvents: ActorRef = catchall + override def Activity: ActorRef = catchall } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) + vehicle.Zone = zone vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + val vehicle2 = Vehicle(GlobalDefinitions.lightning) + vehicle2.Faction = PlanetSideEmpire.TR + vehicle2.GUID = PlanetSideGUID(11) + vehicle2.Zone = zone + vehicle2.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle2), "vehicle2-test") + val player1 = Player(VehicleTest.avatar1) player1.ExoSuit = ExoSuitType.Reinforced player1.GUID = PlanetSideGUID(1) + player1.Zone = zone val player2 = Player(VehicleTest.avatar1) player2.ExoSuit = ExoSuitType.MAX player2.GUID = PlanetSideGUID(2) + player2.Zone = zone val player3 = Player(VehicleTest.avatar1) player3.ExoSuit = ExoSuitType.Agile player3.GUID = PlanetSideGUID(3) + player3.Zone = zone "Vehicle Control" should { - "block players from sitting if their exo-suit is not allowed by the seat" in { - //disallow - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) //Reinforced in non-MAX seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) //MAX in non-Reinforced seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) //MAX in non-MAX seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player1, 9), probe.ref) //Reinforced in MAX-only seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player3, 9), probe.ref) //Agile in MAX-only seat - checkCanNotMount() + "block players from sitting if their exo-suit is not allowed by the mount - apc_tr" in { + val probe = new TestProbe(system) + // disallow + vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) //MAX in non-Max mount + VehicleControlTest.checkCanNotMount(probe, "MAX in non-Max mount 1") + vehicle.Actor.tell(Mountable.TryMount(player2, 2), probe.ref) //MAX in non-MAX mount + VehicleControlTest.checkCanNotMount(probe, "MAX in non-MAX mount 2") + vehicle.Actor.tell(Mountable.TryMount(player1, 11), probe.ref) //Reinforced in MAX-only mount + VehicleControlTest.checkCanNotMount(probe, "Reinforced in MAX-only mount") + vehicle.Actor.tell(Mountable.TryMount(player3, 11), probe.ref) //Agile in MAX-only mount + VehicleControlTest.checkCanNotMount(probe, "Agile in MAX-only mount") //allow - vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) - checkCanMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 9), probe.ref) - checkCanMount() - vehicle.Actor.tell(Mountable.TryMount(player3, 0), probe.ref) - checkCanMount() + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) // Reinforced in driver mount allowing all except MAX + VehicleControlTest.checkCanMount(probe, "Reinforced in driver mount allowing all except MAX") + // Reset to allow further driver mount mounting tests + vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) + probe.receiveOne(500 milliseconds) //discard + vehicle.Owner = None //ensure + vehicle.OwnerName = None //ensure + vehicle.Actor.tell(Mountable.TryMount(player3, 1), probe.ref) // Agile in driver mount allowing all except MAX + VehicleControlTest.checkCanMount(probe, "Agile in driver mount allowing all except MAX") + vehicle.Actor.tell(Mountable.TryMount(player1, 3), probe.ref) // Reinforced in passenger mount allowing all except MAX + VehicleControlTest.checkCanMount(probe, "Reinforced in passenger mount allowing all except MAX") + vehicle.Actor.tell(Mountable.TryMount(player2, 11), probe.ref) // MAX in MAX-only mount + VehicleControlTest.checkCanMount(probe, "MAX in MAX-only mount") } } } class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest { val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) - case _ => - assert(false) - } - } - - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) @@ -416,29 +400,20 @@ class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest { "Vehicle Control" should { //11 June 2018: Group is not supported yet so do not bother testing it - "block players from sitting if the seat does not allow it" in { + "block players from sitting if the mount does not allow it" in { - vehicle.PermissionGroup(2, 3) //passenger group -> empire - vehicle.Actor.tell(Mountable.TryMount(player1, 3), probe.ref) //passenger seat - checkCanMount() - vehicle.PermissionGroup(2, 0) //passenger group -> locked - vehicle.Actor.tell(Mountable.TryMount(player2, 4), probe.ref) //passenger seat - checkCanNotMount() + vehicle.PermissionGroup(2, 3) //passenger group -> empire + vehicle.Actor.tell(Mountable.TryMount(player1, 4), probe.ref) //passenger mount + VehicleControlTest.checkCanMount(probe, "") + vehicle.PermissionGroup(2, 0) //passenger group -> locked + vehicle.Actor.tell(Mountable.TryMount(player2, 5), probe.ref) //passenger mount + VehicleControlTest.checkCanNotMount(probe, "") } } } class VehicleControlMountingDriverSeatTest extends ActorTest { val probe = new TestProbe(system) - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) @@ -447,38 +422,19 @@ class VehicleControlMountingDriverSeatTest extends ActorTest { player1.GUID = PlanetSideGUID(1) "Vehicle Control" should { - "allow players to sit in the driver seat, even if it is locked, if the vehicle is unowned" in { + "allow players to sit in the driver mount, even if it is locked, if the vehicle is unowned" in { assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) assert(vehicle.Owner.isEmpty) - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) + VehicleControlTest.checkCanMount(probe, "") + assert(vehicle.Seats(0).occupant.nonEmpty) } } } class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) - case _ => - assert(false) - } - } - - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) @@ -489,36 +445,27 @@ class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { player2.GUID = PlanetSideGUID(2) "Vehicle Control" should { - "block players that are not the current owner from sitting in the driver seat (locked)" in { + "block players that are not the current owner from sitting in the driver mount (locked)" in { assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) vehicle.Owner = player1.GUID - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) + VehicleControlTest.checkCanMount(probe, "") + assert(vehicle.Seats(0).occupant.nonEmpty) vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) probe.receiveOne(Duration.create(100, "ms")) //discard - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) - checkCanNotMount() - assert(vehicle.Seats(0).Occupant.isEmpty) + vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) + VehicleControlTest.checkCanNotMount(probe, "") + assert(vehicle.Seats(0).occupant.isEmpty) } } } class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { val probe = new TestProbe(system) - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } val vehicle = Vehicle(GlobalDefinitions.apc_tr) vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) @@ -529,22 +476,22 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { player2.GUID = PlanetSideGUID(2) "Vehicle Control" should { - "allow players that are not the current owner to sit in the driver seat (empire)" in { + "allow players that are not the current owner to sit in the driver mount (empire)" in { vehicle.PermissionGroup(0, 3) //passenger group -> empire assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Empire)) //driver group -> empire - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) vehicle.Owner = player1.GUID //owner set - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) + VehicleControlTest.checkCanMount(probe, "") + assert(vehicle.Seats(0).occupant.nonEmpty) vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) probe.receiveOne(Duration.create(100, "ms")) //discard - assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Seats(0).occupant.isEmpty) - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) + VehicleControlTest.checkCanMount(probe, "") + assert(vehicle.Seats(0).occupant.nonEmpty) } } } @@ -655,7 +602,7 @@ class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest { // } // // // val beamer_wep = Tool(GlobalDefinitions.beamer) -// val p_source = PlayerSource( Player(Avatar(0, "TestTarget", PlanetSideEmpire.NC, CharacterGender.Female, 1, CharacterVoice.Mute)) ) +// val p_source = PlayerSource( Player(Avatar(0, "TestTarget", PlanetSideEmpire.NC, CharacterSex.Female, 1, CharacterVoice.Mute)) ) // val projectile = Projectile(beamer_wep.Projectile, GlobalDefinitions.beamer, beamer_wep.FireMode, p_source, GlobalDefinitions.beamer.ObjectId, Vector3.Zero, Vector3.Zero) // val fury_dm = Vehicle(GlobalDefinitions.fury).DamageModel // val obj = DamageInteraction(p_source, ProjectileReason(DamageResolution.Hit, projectile, fury_dm), Vector3(1.2f, 3.4f, 5.6f)) @@ -677,7 +624,7 @@ class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest { class VehicleControlInteractWithWaterPartialTest extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val playerProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) @@ -700,7 +647,7 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID player1.Actor = playerProbe.ref vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") @@ -728,7 +675,7 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest { class VehicleControlInteractWithWaterTest extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val avatarProbe = TestProbe() val vehicleProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) @@ -755,7 +702,7 @@ class VehicleControlInteractWithWaterTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") @@ -799,7 +746,7 @@ class VehicleControlInteractWithWaterTest extends ActorTest { class VehicleControlStopInteractWithWaterTest extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val playerProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) @@ -822,7 +769,7 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID player1.Actor = playerProbe.ref vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") @@ -863,7 +810,7 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest { class VehicleControlInteractWithLavaTest extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val avatarProbe = TestProbe() val vehicleProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) @@ -891,7 +838,7 @@ class VehicleControlInteractWithLavaTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") @@ -924,7 +871,7 @@ class VehicleControlInteractWithLavaTest extends ActorTest { class VehicleControlInteractWithDeathTest extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) //guid=1 val guid = new NumberPoolHub(new MaxNumberSource(15)) val pool = Pool(EnvironmentAttribute.Death, DeepSquare(-1, 10, 10, 0, 0)) val zone = new Zone( @@ -949,7 +896,7 @@ class VehicleControlInteractWithDeathTest extends ActorTest { player1.Spawn() vehicle.Zone = zone vehicle.Faction = PlanetSideEmpire.TR - vehicle.Seats(0).Occupant = player1 + vehicle.Seats(0).mount(player1) player1.VehicleSeated = vehicle.GUID val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") @@ -971,8 +918,28 @@ class VehicleControlInteractWithDeathTest extends ActorTest { object VehicleControlTest { import net.psforever.objects.avatar.Avatar - import net.psforever.types.{CharacterGender, PlanetSideEmpire} + import net.psforever.types.{CharacterSex, PlanetSideEmpire} - val avatar1 = Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) - val avatar2 = Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + val avatar1 = Avatar(0, "test1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) + val avatar2 = Avatar(1, "test2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) + + def checkCanNotMount(probe: TestProbe, id: String): Unit = { + val reply = probe.receiveOne(Duration.create(250, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanNotMount], s"test $id") + case _ => + assert(false, s"test $id-b") + } + } + + def checkCanMount(probe: TestProbe, id: String): Unit = { + val reply = probe.receiveOne(Duration.create(250, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount], s" - test: $id") + case _ => + assert(false, s" - test: $id-b") + } + } } diff --git a/src/test/scala/objects/VehicleTest.scala b/src/test/scala/objects/VehicleTest.scala index cb5a860a..3293946b 100644 --- a/src/test/scala/objects/VehicleTest.scala +++ b/src/test/scala/objects/VehicleTest.scala @@ -2,7 +2,8 @@ package objects import net.psforever.objects._ -import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition} +import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.serverobject.mount._ import net.psforever.objects.vehicles._ import net.psforever.types.{PlanetSideGUID, _} import org.specs2.mutable._ @@ -12,35 +13,31 @@ class VehicleTest extends Specification { "SeatDefinition" should { val seat = new SeatDefinition - seat.ArmorRestriction = SeatArmorRestriction.MaxOnly - seat.Bailable = true - seat.ControlledWeapon = 5 + seat.restriction = MaxOnly + seat.bailable = true "define (default)" in { val t_seat = new SeatDefinition - t_seat.ArmorRestriction mustEqual SeatArmorRestriction.NoMax - t_seat.Bailable mustEqual false - t_seat.ControlledWeapon.isEmpty mustEqual true + t_seat.restriction mustEqual NoMax + t_seat.bailable mustEqual false } "define (custom)" in { - seat.ArmorRestriction mustEqual SeatArmorRestriction.MaxOnly - seat.Bailable mustEqual true - seat.ControlledWeapon.contains(5) + seat.restriction mustEqual MaxOnly + seat.bailable mustEqual true } } "VehicleDefinition" should { "define" in { val fury = GlobalDefinitions.fury - fury.CanBeOwned mustEqual true + fury.CanBeOwned.contains(true) mustEqual true fury.CanCloak mustEqual false fury.Seats.size mustEqual 1 - fury.Seats(0).Bailable mustEqual true - fury.Seats(0).ControlledWeapon.contains(1) + fury.Seats(0).bailable mustEqual true fury.MountPoints.size mustEqual 2 - fury.MountPoints.get(1).contains(0) - fury.MountPoints.get(2).contains(0) + fury.MountPoints.get(1).contains(MountInfo(0, Vector3(0,0,0))) mustEqual true + fury.MountPoints.get(2).contains(MountInfo(0, Vector3(0,0,0))) mustEqual true fury.Weapons.size mustEqual 1 fury.Weapons.get(0).isEmpty mustEqual true fury.Weapons.get(1).contains(GlobalDefinitions.fury_weapon_systema) @@ -52,69 +49,67 @@ class VehicleTest extends Specification { "Seat" should { val seat_def = new SeatDefinition - seat_def.ArmorRestriction = SeatArmorRestriction.MaxOnly - seat_def.Bailable = true - seat_def.ControlledWeapon = 5 + seat_def.restriction = MaxOnly + seat_def.bailable = true "construct" in { val seat = new Seat(seat_def) - seat.ArmorRestriction mustEqual SeatArmorRestriction.MaxOnly - seat.Bailable mustEqual true - seat.ControlledWeapon.contains(5) + seat.definition.restriction mustEqual MaxOnly + seat.bailable mustEqual true seat.isOccupied mustEqual false - seat.Occupant.isEmpty mustEqual true + seat.occupant.isEmpty mustEqual true } "player can sit" in { val seat = new Seat(seat_def) - seat.Occupant.isDefined mustEqual false + seat.isOccupied mustEqual false val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX - seat.Occupant = player1 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player1) mustEqual true + seat.mount(player1) + seat.isOccupied mustEqual true + seat.occupant.contains(player1) mustEqual true } "one occupant at a time" in { val seat = new Seat(seat_def) - seat.Occupant.isDefined mustEqual false + seat.isOccupied mustEqual false val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX - seat.Occupant = player1 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player1) mustEqual true + seat.mount(player1) + seat.isOccupied mustEqual true + seat.occupant.contains(player1) mustEqual true val player2 = Player(avatar1) player2.ExoSuit = ExoSuitType.MAX - seat.Occupant = player2 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player1) mustEqual true + seat.mount(player2) + seat.isOccupied mustEqual true + seat.occupant.contains(player1) mustEqual true } - "one player must get out of seat before other can get in" in { + "one player must get out of mount before other can get in" in { val seat = new Seat(seat_def) - seat.Occupant.isDefined mustEqual false + seat.isOccupied mustEqual false val player1 = Player(avatar1) player1.ExoSuit = ExoSuitType.MAX - seat.Occupant = player1 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player1) mustEqual true + seat.mount(player1) + seat.isOccupied mustEqual true + seat.occupant.contains(player1) mustEqual true val player2 = Player(avatar2) player2.ExoSuit = ExoSuitType.MAX - seat.Occupant = player2 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player2) mustEqual false - seat.Occupant.contains(player1) mustEqual true + seat.mount(player2) + seat.isOccupied mustEqual true + seat.occupant.contains(player2) mustEqual false + seat.occupants.contains(player1) mustEqual true - seat.Occupant = None - seat.Occupant.isDefined mustEqual false - seat.Occupant = player2 - seat.Occupant.isDefined mustEqual true - seat.Occupant.contains(player2) mustEqual true + seat.unmount(player1) + seat.isOccupied mustEqual false + seat.mount(player2) + seat.isOccupied mustEqual true + seat.occupants.contains(player2) mustEqual true } } @@ -128,11 +123,10 @@ class VehicleTest extends Specification { val fury_vehicle = Vehicle(GlobalDefinitions.fury) fury_vehicle.Owner.isEmpty mustEqual true fury_vehicle.Seats.size mustEqual 1 - fury_vehicle.Seats(0).ArmorRestriction mustEqual SeatArmorRestriction.NoMax + fury_vehicle.Seats(0).definition.restriction mustEqual NoMax fury_vehicle.Seats(0).isOccupied mustEqual false - fury_vehicle.Seats(0).Occupant.isEmpty mustEqual true - fury_vehicle.Seats(0).Bailable mustEqual true - fury_vehicle.Seats(0).ControlledWeapon.contains(1) + fury_vehicle.Seats(0).occupants.isEmpty mustEqual true + fury_vehicle.Seats(0).bailable mustEqual true fury_vehicle.PermissionGroup(0).contains(VehicleLockState.Locked) //driver fury_vehicle.PermissionGroup(1).contains(VehicleLockState.Empire) //gunner fury_vehicle.PermissionGroup(2).contains(VehicleLockState.Empire) //passenger @@ -180,7 +174,7 @@ class VehicleTest extends Specification { fury_vehicle.Owner.contains(PlanetSideGUID(2)) mustEqual true } - "can use mount point to get seat number" in { + "can use mount point to get mount number" in { val fury_vehicle = Vehicle(GlobalDefinitions.fury) fury_vehicle.GetSeatFromMountPoint(0).isEmpty mustEqual true fury_vehicle.GetSeatFromMountPoint(1).contains(0) @@ -224,7 +218,7 @@ class VehicleTest extends Specification { fury_vehicle.PermissionGroup(AccessPermissionGroup.Driver.id) mustEqual fury_vehicle.PermissionGroup(10) } - "can determine permission group from seat" in { + "can determine permission group from mount" in { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) harasser_vehicle.SeatPermissionGroup(0).contains(AccessPermissionGroup.Driver) harasser_vehicle.SeatPermissionGroup(1).contains(AccessPermissionGroup.Gunner) @@ -232,23 +226,23 @@ class VehicleTest extends Specification { //TODO test for AccessPermissionGroup.Passenger later } - "can find a passenger in a seat" in { + "can find a passenger in a mount" in { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) val player1 = Player(avatar1) player1.GUID = PlanetSideGUID(1) val player2 = Player(avatar2) player2.GUID = PlanetSideGUID(2) - harasser_vehicle.Seat(0).get.Occupant = player1 //don't worry about ownership for now - harasser_vehicle.Seat(1).get.Occupant = player2 + harasser_vehicle.Seat(0).get.mount(player1) //don't worry about ownership for now + harasser_vehicle.Seat(1).get.mount(player2) harasser_vehicle.PassengerInSeat(player1).contains(0) harasser_vehicle.PassengerInSeat(player2).contains(1) - harasser_vehicle.Seat(0).get.Occupant = None + harasser_vehicle.Seat(0).get.unmount(player1) harasser_vehicle.PassengerInSeat(player1).isEmpty mustEqual true harasser_vehicle.PassengerInSeat(player2).contains(1) } - "can find a weapon controlled from seat" in { + "can find a weapon controlled from mount" in { val harasser_vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) val chaingun_p = harasser_vehicle.Weapons(2).Equipment chaingun_p.isDefined mustEqual true @@ -313,8 +307,8 @@ class VehicleTest extends Specification { object VehicleTest { import net.psforever.objects.avatar.Avatar - import net.psforever.types.{CharacterGender, PlanetSideEmpire} + import net.psforever.types.{CharacterSex, PlanetSideEmpire} - val avatar1 = Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) - val avatar2 = Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + val avatar1 = Avatar(0, "test1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) + val avatar2 = Avatar(1, "test2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) } diff --git a/src/test/scala/objects/VitalityTest.scala b/src/test/scala/objects/VitalityTest.scala index e8aab79b..da8a2408 100644 --- a/src/test/scala/objects/VitalityTest.scala +++ b/src/test/scala/objects/VitalityTest.scala @@ -20,7 +20,7 @@ class VitalityTest extends Specification { val vSource = VehicleSource(vehicle) "accept a variety of events" in { - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val pSource = PlayerSource(player) val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero) val resprojectile = DamageInteraction( @@ -47,7 +47,7 @@ class VitalityTest extends Specification { } "return and clear the former list of vital activities" in { - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val pSource = PlayerSource(player) player.History(HealFromKit(pSource, 10, GlobalDefinitions.medkit)) @@ -71,7 +71,7 @@ class VitalityTest extends Specification { } "get exactly one entry that was caused by projectile damage" in { - val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val pSource = PlayerSource(player) val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero) val resprojectile = DamageInteraction( diff --git a/src/test/scala/objects/ZoneTest.scala b/src/test/scala/objects/ZoneTest.scala index e754e594..77a5bd78 100644 --- a/src/test/scala/objects/ZoneTest.scala +++ b/src/test/scala/objects/ZoneTest.scala @@ -217,7 +217,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -237,7 +237,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val avatar = Avatar(1, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val avatar = Avatar(1, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -257,7 +257,7 @@ class ZonePopulationTest extends ActorTest { /* TODO they need AvatarActor, which has further dependencies "associate user with a character" in { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -278,7 +278,7 @@ class ZonePopulationTest extends ActorTest { "disassociate character from a user" in { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -301,7 +301,7 @@ class ZonePopulationTest extends ActorTest { "user tries to Leave, but still has an associated character" in { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -326,7 +326,7 @@ class ZonePopulationTest extends ActorTest { "user tries to Spawn a character, but an associated character already exists" in { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val player1 = Player(avatar) player1.GUID = PlanetSideGUID(1) val player2 = Player(avatar) @@ -354,7 +354,7 @@ class ZonePopulationTest extends ActorTest { "user tries to Spawn a character, but did not Join first" in { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -376,7 +376,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val avatar = Avatar(2, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val avatar = Avatar(2, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -402,7 +402,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val player = Player(Avatar(3, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + val player = Player(Avatar(3, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5)) player.GUID = PlanetSideGUID(1) player.Release zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -419,7 +419,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val player = Player(Avatar(4, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + val player = Player(Avatar(4, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5)) player.GUID = PlanetSideGUID(1) player.Release zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -438,13 +438,13 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val player1 = Player(Avatar(5, "Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + val player1 = Player(Avatar(5, "Chord1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5)) player1.GUID = PlanetSideGUID(1) player1.Release - val player2 = Player(Avatar(6, "Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + val player2 = Player(Avatar(6, "Chord2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5)) player2.GUID = PlanetSideGUID(2) player2.Release - val player3 = Player(Avatar(7, "Chord3", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + val player3 = Player(Avatar(7, "Chord3", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5)) player3.GUID = PlanetSideGUID(3) player3.Release zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) @@ -469,7 +469,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } - val player = Player(Avatar(8, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + val player = Player(Avatar(8, "Chord", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Voice5)) player.GUID = PlanetSideGUID(1) //player.Release !!important zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) diff --git a/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala b/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala index d791a6b8..4f4171fa 100644 --- a/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala +++ b/src/test/scala/objects/guidtask/GUIDTaskRegisterAvatarTest.scala @@ -6,12 +6,12 @@ import net.psforever.objects._ import net.psforever.objects.avatar.Avatar import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.objects.locker.LockerEquipment -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire} class GUIDTaskRegisterAvatarTest extends ActorTest { "RegisterAvatar" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala b/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala index 53ad4752..bb9ba900 100644 --- a/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala +++ b/src/test/scala/objects/guidtask/GUIDTaskRegisterPlayerTest.scala @@ -6,12 +6,12 @@ import net.psforever.objects._ import net.psforever.objects.avatar.Avatar import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.objects.locker.LockerEquipment -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire} class GUIDTaskRegisterPlayerTest extends ActorTest { "RegisterPlayer" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala b/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala index aff46a39..9121e317 100644 --- a/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala +++ b/src/test/scala/objects/guidtask/GUIDTaskUnregisterAvatarTest.scala @@ -6,12 +6,12 @@ import net.psforever.objects._ import net.psforever.objects.avatar.Avatar import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.objects.locker.LockerEquipment -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire} class GUIDTaskUnregisterAvatarTest extends ActorTest { "UnregisterAvatar" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala b/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala index 4c62dc0b..a10df710 100644 --- a/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala +++ b/src/test/scala/objects/guidtask/GUIDTaskUnregisterPlayerTest.scala @@ -6,12 +6,12 @@ import net.psforever.objects._ import net.psforever.objects.avatar.Avatar import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.objects.locker.LockerEquipment -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire} class GUIDTaskUnregisterPlayerTest extends ActorTest { "UnregisterPlayer" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val obj = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala b/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala index 8e559766..eb046576 100644 --- a/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala +++ b/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala @@ -5,16 +5,14 @@ import akka.actor.{ActorSystem, Props} import base.ActorTest import net.psforever.objects.avatar.Avatar import net.psforever.objects.{Default, GlobalDefinitions, Player} -import net.psforever.objects.definition.SeatDefinition import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource -import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalMech, ImplantTerminalMechControl} +import net.psforever.objects.serverobject.mount.{MountInfo, Mountable, Seat, SeatDefinition} import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.terminals.Terminal -import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalMech, ImplantTerminalMechControl} -import net.psforever.objects.vehicles.Seat import net.psforever.objects.zones.{Zone, ZoneMap} -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification import scala.concurrent.duration.Duration @@ -24,14 +22,10 @@ class ImplantTerminalMechTest extends Specification { "define" in { val implant_terminal_mech = GlobalDefinitions.implant_terminal_mech implant_terminal_mech.ObjectId mustEqual 410 - implant_terminal_mech.MountPoints mustEqual Map(1 -> 0) + implant_terminal_mech.MountPoints.get(1).contains(MountInfo(0, Vector3.Zero)) mustEqual true implant_terminal_mech.Seats.keySet mustEqual Set(0) implant_terminal_mech.Seats(0).isInstanceOf[SeatDefinition] mustEqual true - implant_terminal_mech - .Seats(0) - .ArmorRestriction mustEqual net.psforever.objects.vehicles.SeatArmorRestriction.NoMax - implant_terminal_mech.Seats(0).Bailable mustEqual false - implant_terminal_mech.Seats(0).ControlledWeapon.isEmpty mustEqual true + implant_terminal_mech.Seats(0).bailable mustEqual false } } @@ -44,20 +38,20 @@ class ImplantTerminalMechTest extends Specification { obj.Seats(0).isInstanceOf[Seat] mustEqual true } - "get seat from mount points" in { + "get mount from mount points" in { val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) obj.GetSeatFromMountPoint(0).isEmpty mustEqual true obj.GetSeatFromMountPoint(1).contains(0) mustEqual true obj.GetSeatFromMountPoint(2).isEmpty mustEqual true } - "get passenger in a seat" in { - val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + "get passenger in a mount" in { + val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) obj.PassengerInSeat(player).isEmpty mustEqual true - obj.Seats(0).Occupant = player + obj.Seats(0).mount(player) obj.PassengerInSeat(player).contains(0) mustEqual true - obj.Seats(0).Occupant = None + obj.Seats(0).unmount(player) obj.PassengerInSeat(player).isEmpty mustEqual true } } @@ -77,7 +71,7 @@ class ImplantTerminalMechControl2Test extends ActorTest { "ImplantTerminalMechControl" should { "let a player mount" in { val (player, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - val msg = Mountable.TryMount(player, 0) + val msg = Mountable.TryMount(player, 1) mech.Actor ! msg val reply = receiveOne(Duration.create(200, "ms")) @@ -87,22 +81,22 @@ class ImplantTerminalMechControl2Test extends ActorTest { assert(reply2.response.isInstanceOf[Mountable.CanMount]) val reply3 = reply2.response.asInstanceOf[Mountable.CanMount] assert(reply3.obj == mech) - assert(reply3.seat_num == 0) + assert(reply3.seat_number == 0) } } } class ImplantTerminalMechControl3Test extends ActorTest { - import net.psforever.types.CharacterGender + import net.psforever.types.CharacterSex "ImplantTerminalMechControl" should { "block a player from mounting" in { val (player1, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - val player2 = Player(Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player2 = Player(Avatar(1, "test2", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) - mech.Actor ! Mountable.TryMount(player1, 0) + mech.Actor ! Mountable.TryMount(player1, 1) receiveOne(Duration.create(100, "ms")) //consume reply - mech.Actor ! Mountable.TryMount(player2, 0) + mech.Actor ! Mountable.TryMount(player2, 1) val reply = receiveOne(Duration.create(100, "ms")) assert(reply.isInstanceOf[Mountable.MountMessages]) val reply2 = reply.asInstanceOf[Mountable.MountMessages] @@ -110,7 +104,7 @@ class ImplantTerminalMechControl3Test extends ActorTest { assert(reply2.response.isInstanceOf[Mountable.CanNotMount]) val reply3 = reply2.response.asInstanceOf[Mountable.CanNotMount] assert(reply3.obj == mech) - assert(reply3.seat_num == 0) + assert(reply3.mount_point == 1) } } } @@ -119,7 +113,7 @@ class ImplantTerminalMechControl4Test extends ActorTest { "ImplantTerminalMechControl" should { "dismount player after mounting" in { val (player, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - mech.Actor ! Mountable.TryMount(player, 0) + mech.Actor ! Mountable.TryMount(player, 1) receiveOne(Duration.create(200, "ms")) //consume reply assert(mech.Seat(0).get.isOccupied) @@ -141,11 +135,11 @@ class ImplantTerminalMechControl5Test extends ActorTest { "ImplantTerminalMechControl" should { "block a player from dismounting" in { val (player, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - mech.Actor ! Mountable.TryMount(player, 0) + mech.Actor ! Mountable.TryMount(player, 1) receiveOne(Duration.create(100, "ms")) //consume reply assert(mech.Seat(0).get.isOccupied) - mech.Velocity = Vector3(1, 0, 0) //makes no sense, but it works as the "seat" is not bailable + mech.Velocity = Vector3(1, 0, 0) //makes no sense, but it works as the "mount" is not bailable mech.Actor ! Mountable.TryDismount(player, 0) val reply = receiveOne(Duration.create(100, "ms")) assert(reply.isInstanceOf[Mountable.MountMessages]) @@ -189,6 +183,6 @@ object ImplantTerminalMechTest { map.linkTerminalToInterface(1, 2) terminal.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], terminal), "terminal-control") - (Player(Avatar(0, "test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), terminal) + (Player(Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute)), terminal) } } diff --git a/src/test/scala/objects/terminal/MatrixTerminalTest.scala b/src/test/scala/objects/terminal/MatrixTerminalTest.scala index 2a531556..47f1ac36 100644 --- a/src/test/scala/objects/terminal/MatrixTerminalTest.scala +++ b/src/test/scala/objects/terminal/MatrixTerminalTest.scala @@ -21,7 +21,7 @@ class MatrixTerminalTest extends Specification { } "invalid message" in { - val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "lite_armor", 0, PlanetSideGUID(0)) val terminal = Terminal(new MatrixTerminalDefinition(519)) terminal.Owner = Vehicle(GlobalDefinitions.quadstealth) diff --git a/src/test/scala/objects/terminal/OrderTerminalTest.scala b/src/test/scala/objects/terminal/OrderTerminalTest.scala index dd4a3a8a..51f5df9e 100644 --- a/src/test/scala/objects/terminal/OrderTerminalTest.scala +++ b/src/test/scala/objects/terminal/OrderTerminalTest.scala @@ -11,7 +11,7 @@ import net.psforever.types._ import org.specs2.mutable.Specification class OrderTerminalTest extends Specification { - val avatar = Avatar(0, "test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + val avatar = Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute) val player = Player(avatar) val building = new Building( diff --git a/src/test/scala/objects/terminal/ProximityTest.scala b/src/test/scala/objects/terminal/ProximityTest.scala index b7f31d67..8df74b1c 100644 --- a/src/test/scala/objects/terminal/ProximityTest.scala +++ b/src/test/scala/objects/terminal/ProximityTest.scala @@ -17,7 +17,7 @@ import net.psforever.objects.serverobject.terminals.{ } import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects.{GlobalDefinitions, Player} -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, PlanetSideGUID} +import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, PlanetSideGUID} import org.specs2.mutable.Specification import net.psforever.services.Service import net.psforever.services.local.LocalService @@ -41,7 +41,7 @@ class ProximityTest extends Specification { ProximityTest.avatarId.getAndIncrement(), "TestCharacter1", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -54,7 +54,7 @@ class ProximityTest extends Specification { ProximityTest.avatarId.getAndIncrement(), "TestCharacter2", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -77,7 +77,7 @@ class ProximityTest extends Specification { ProximityTest.avatarId.getAndIncrement(), "TestCharacter1", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -90,7 +90,7 @@ class ProximityTest extends Specification { ProximityTest.avatarId.getAndIncrement(), "TestCharacter2", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -118,7 +118,7 @@ class ProximityTest extends Specification { ProximityTest.avatarId.getAndIncrement(), "TestCharacter1", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -140,7 +140,7 @@ class ProximityTest extends Specification { ProximityTest.avatarId.getAndIncrement(), "TestCharacter1", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -184,7 +184,7 @@ class ProximityTerminalControlStartTest extends ActorTest { ProximityTest.avatarId.getAndIncrement(), "TestCharacter1", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -235,7 +235,7 @@ class ProximityTerminalControlTwoUsersTest extends ActorTest { ProximityTest.avatarId.getAndIncrement(), "TestCharacter1", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -249,7 +249,7 @@ class ProximityTerminalControlTwoUsersTest extends ActorTest { ProximityTest.avatarId.getAndIncrement(), "TestCharacter2", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -307,7 +307,7 @@ class ProximityTerminalControlStopTest extends ActorTest { ProximityTest.avatarId.getAndIncrement(), "TestCharacter1", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -361,7 +361,7 @@ class ProximityTerminalControlNotStopTest extends ActorTest { ProximityTest.avatarId.getAndIncrement(), "TestCharacter1", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) @@ -375,7 +375,7 @@ class ProximityTerminalControlNotStopTest extends ActorTest { ProximityTest.avatarId.getAndIncrement(), "TestCharacter2", PlanetSideEmpire.VS, - CharacterGender.Female, + CharacterSex.Female, 1, CharacterVoice.Voice1 ) diff --git a/src/test/scala/objects/terminal/TerminalControlTest.scala b/src/test/scala/objects/terminal/TerminalControlTest.scala index 3f8c45ae..b236b5c2 100644 --- a/src/test/scala/objects/terminal/TerminalControlTest.scala +++ b/src/test/scala/objects/terminal/TerminalControlTest.scala @@ -143,6 +143,6 @@ object TerminalControlTest { GlobalDefinitions.building ) terminal.Owner.Faction = faction - (Player(Avatar(0, "test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), terminal) + (Player(Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute)), terminal) } } diff --git a/src/test/scala/service/HartServiceTest.scala b/src/test/scala/service/HartServiceTest.scala new file mode 100644 index 00000000..3952a8f7 --- /dev/null +++ b/src/test/scala/service/HartServiceTest.scala @@ -0,0 +1,36 @@ +// Copyright (c) 2021 PSForever +package service + +import akka.actor.{ActorRef, Props} +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.services.hart.{HartService, HartTimer} +import net.psforever.types.PlanetSideGUID + +import scala.concurrent.duration._ + +class HartServiceTest extends ActorTest { + "HartService" should { + val hart = system.actorOf(Props[HartService](), name = "hart") + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), zoneNumber = 0) { + override def SetupNumberPools(): Unit = {} + override def AvatarEvents: ActorRef = catchall + override def LocalEvents: ActorRef = catchall + override def VehicleEvents: ActorRef = catchall + override def Activity: ActorRef = catchall + } + + "pass messages back upon pairing" in { + val probe = new TestProbe(system) + hart ! HartTimer.PairWith(zone, PlanetSideGUID(1), PlanetSideGUID(2), probe.ref) + probe.receiveOne(max = 1 seconds) match { + case HartTimer.ShuttleDocked("test") => assert(true) + case _ => assert(false) + } + } + } +} + +object HartServiceTest { /* initially left empty */ } diff --git a/src/test/scala/service/HartTimerTest.scala b/src/test/scala/service/HartTimerTest.scala new file mode 100644 index 00000000..038909b1 --- /dev/null +++ b/src/test/scala/service/HartTimerTest.scala @@ -0,0 +1,57 @@ +// Copyright (c) 2021 PSForever +package service + +import akka.actor.Props +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.services.hart.HartTimer +import net.psforever.types.PlanetSideGUID + +import scala.concurrent.duration._ + +class HartTimerNotScheduled extends ActorTest { + "HartTimer" should { + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + override def AvatarEvents = catchall + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + } + val timer = system.actorOf(Props(classOf[HartTimer], zone), "hart-timer") + + "not do anything if paired before having a schedule set" in { + val probe = new TestProbe(system) + timer ! HartTimer.PairWith(zone, PlanetSideGUID(1), PlanetSideGUID(2), probe.ref) + probe.expectNoMessage(max = 3 seconds) + } + } +} + +class HartTimerInitializedPairingScheduled extends ActorTest { + "HartTimer" should { + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + override def AvatarEvents = catchall + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + } + val timer = system.actorOf(Props(classOf[HartTimer], zone), "hart-timer") + + "perform some initialization when paired" in { + val probe = new TestProbe(system) + timer ! HartTimer.SetEventDurations("test", 55000, 10000) + timer ! HartTimer.PairWith(zone, PlanetSideGUID(1), PlanetSideGUID(2), probe.ref) + probe.receiveOne(1 seconds) match { + case HartTimer.ShuttleDocked("test") => assert(true) + case _ => assert(false) + } + } + } +} + +object HartTimerTest { /* initially left empty */ }