Merge branch 'master-upstream' into llu-2021

# Conflicts:
#	src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
#	src/main/scala/net/psforever/services/galaxy/GalaxyService.scala
#	src/main/scala/net/psforever/services/local/LocalService.scala
#	src/main/scala/net/psforever/services/local/LocalServiceMessage.scala
This commit is contained in:
Mazo 2021-03-31 23:12:59 +01:00
commit 7e1466c898
208 changed files with 7345 additions and 2869 deletions

View file

@ -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"

View file

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

View file

@ -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`.

View file

@ -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.*"

View file

@ -10,36 +10,116 @@
</filter>
</appender>
<appender name="FILE-DEBUG" class="ch.qos.logback.core.FileAppender">
<file>logs/pslogin-debug_${bySecond}.log</file>
<appender name="FILE-GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/psforever-general_${bySecond}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/psforever-general_%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>60</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%date{ISO8601} %5level "%X" %logger{35} - %msg%n</pattern>
<pattern>%date{ISO8601} %5level %logger{35} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<matcher>
<Name>encrypted</Name>
<!-- occurs during latency or relogging complications; the messages are useless -->
<regex>Unexpected packet type EncryptedPacket</regex>
</matcher>
<expression>encrypted.matches(formattedMessage)</expression>
</evaluator>
<OnMatch>DENY</OnMatch>
<OnMismatch>NEUTRAL</OnMismatch>
</filter>
<filter class="net.psforever.filters.LoggerPrefixFilter">
<!--
c.g.j.s.d.p.c.PostgreSQLConnectionHandler
-->
<prefix>com.github.jasync.sql.db.postgresql.codec</prefix>
</filter>
<filter class="net.psforever.filters.LoggerPrefixFilter">
<!--
i.s.c.AbstractConnection.lockdown
i.sentry.connection.AsyncConnection
-->
<prefix>io.sentry.connection</prefix>
</filter>
<filter class="net.psforever.filters.LoggerPrefixFilter">
<!-- damage log -->
<prefix>DamageResolution</prefix>
</filter>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
<level>INFO</level>
</filter>
<filter class="net.psforever.filters.ApplyCooldownToDuplicateLoggingFilter">
<cooldown>5000</cooldown>
</filter>
</appender>
<appender name="FILE-TRACE" class="ch.qos.logback.core.FileAppender">
<file>logs/pslogin-trace_${bySecond}.log</file>
<appender name="FILE-DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/psforever-debug_${bySecond}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/psforever-debug_%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>60</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%date{ISO8601} [%thread] %5level "%X" %logger{35} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>OFF</level>
<!--<level>TRACE</level>-->
<filter class="net.psforever.filters.LoggerPrefixFilter">
<!--
c.g.j.s.d.p.e.CloseStatementEncoder
c.g.j.s.d.p.e.PreparedStatementEncoderHelper
c.g.j.s.d.p.e.PreparedStatementOpeningEncoder
c.g.j.s.d.p.e.QueryMessageEncoder
-->
<prefix>com.github.jasync.sql.db.postgresql.encoders</prefix>
</filter>
<filter class="net.psforever.filters.LoggerPrefixFilter">
<!--
c.g.j.s.d.p.c.PostgreSQLConnectionHandler
consider: c.g.j.s.d.p.PostgreSQLConnection?
-->
<prefix>com.github.jasync.sql.db.postgresql.codec</prefix>
</filter>
<filter class="net.psforever.filters.LoggerPrefixFilter">
<!-- i.g.context.jasync.JAsyncContext -->
<prefix>io.getquill.context.jasync</prefix>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- <filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter> -->
</appender>
<appender name="Sentry" class="io.sentry.logback.SentryAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<matcher>
<Name>encrypted</Name>
<!-- occurs during logging or relogging complications; the messages are useless -->
<regex>Unexpected packet type EncryptedPacket</regex>
</matcher>
<expression>encrypted.matches(formattedMessage)</expression>
</evaluator>
<OnMatch>DENY</OnMatch>
<OnMismatch>NEUTRAL</OnMismatch>
</filter>
</appender>
<root level="TRACE">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE-TRACE"/>
<appender-ref ref="FILE-GENERAL"/>
<appender-ref ref="FILE-DEBUG"/>
<appender-ref ref="Sentry"/>
</root>

View file

@ -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")

View file

@ -14,6 +14,10 @@
{
"groupName": "circe",
"packagePatterns": "^io.circe"
},
{
"groupName": "kamon",
"packagePatterns": "^io.kamon"
}
]
}

View file

@ -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<ILoggingEvent> {
private long cooldown;
private ConcurrentHashMap<String, Long> 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<String> 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();
}
}

View file

@ -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<ILoggingEvent> {
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();
}
}
}

View file

@ -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

View file

@ -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")

View file

@ -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
),

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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 {

View file

@ -82,7 +82,7 @@ object SocketActor {
socketActor ! toSocket(message)
}
} else {
log.info("Network simulator dropped packet")
log.trace("Network simulator dropped packet")
}
}

View file

@ -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 {

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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, _, _, _)) =>

View file

@ -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)
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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)

View file

@ -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.<br>
* <br>
@ -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.)<br>
* <br>
* 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] {

View file

@ -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)
}
}

View file

@ -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,

View file

@ -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.

View file

@ -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)
)
}

View file

@ -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 {

View file

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

View file

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

View file

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

View file

@ -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<br>
* `Some(true)` - assign ownership upon the driver mount, maintains ownership after the driver dismounts<br>
* `Some(false)` - assign ownership upon the driver mount, becomes unowned after the driver dismounts<br>
* `None` - does not assign ownership<br>
* 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 = {

View file

@ -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,

View file

@ -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)"))
}

View file

@ -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

View file

@ -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
)
)
}

View file

@ -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 =>

View file

@ -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

View file

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

View file

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

View file

@ -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)
}

View file

@ -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}")
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)

View file

@ -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 => ;

View file

@ -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)
}

View file

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

View file

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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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.

View file

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

View file

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

View file

@ -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.

View file

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

View file

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

View file

@ -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]
}

View file

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

View file

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

View file

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

View file

@ -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 {

View file

@ -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

View file

@ -14,7 +14,7 @@ import scala.concurrent.duration._
* <br>
* 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
*/

View file

@ -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) =>

View file

@ -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.<br>
* <br>
* 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

View file

@ -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) {

View file

@ -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,

View file

@ -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.<br>
* <br>
* 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
}
}
}

View file

@ -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.<br>
* <br>
* 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
}
}

View file

@ -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`.<br>
* <br>
* 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
}
}

View file

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

View file

@ -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}")
}
}
}

View file

@ -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)
})
}
}

View file

@ -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 {

View file

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

View file

@ -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))

View file

@ -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)
}

View file

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

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -3,9 +3,9 @@ package net.psforever.objects.vehicles
/**
* An `Enumeration` of various permission groups that control access to aspects of a vehicle.<br>
* - `Driver` is a seat that is always seat number 0.<br>
* - `Gunner` is a seat that is not the `Driver` and controls a mounted weapon.<br>
* - `Passenger` is a seat that is not the `Driver` and does not have control of a mounted weapon.<br>
* - `Driver` is a mount that is always mount number 0.<br>
* - `Gunner` is a mount that is not the `Driver` and controls a mounted weapon.<br>
* - `Passenger` is a mount that is not the `Driver` and does not have control of a mounted weapon.<br>
* - `Trunk` represnts access to the vehicle's internal storage space.<br>
* 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.

View file

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

View file

@ -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()

View file

@ -2,11 +2,11 @@
package net.psforever.objects.vehicles
/**
* An `Enumeration` of exo-suit-based seat access restrictions.<br>
* An `Enumeration` of exo-suit-based mount access restrictions.<br>
* <br>
* 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

View file

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

View file

@ -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]()
}

View file

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

Some files were not shown because too many files have changed in this diff Show more