mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +00:00
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:
commit
7e1466c898
10
.codecov.yml
10
.codecov.yml
|
|
@ -1,6 +1,13 @@
|
|||
# Too spammy for us
|
||||
comment: off
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.25%
|
||||
|
||||
ignore:
|
||||
- "src/main/scala/net/psforever/objects/ObjectType.scala"
|
||||
- "src/main/scala/net/psforever/objects/avatar/Avatars.scala"
|
||||
|
|
@ -14,6 +21,7 @@ ignore:
|
|||
- "src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/pad/AutoDriveControls.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/shuttle/ShuttleAmenity.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/turret/TurretUpgrade.scala"
|
||||
- "src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala"
|
||||
- "src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala"
|
||||
|
|
@ -63,6 +71,8 @@ ignore:
|
|||
- "src/main/scala/net/psforever/services/avatar/AvatarResponse.scala"
|
||||
- "src/main/scala/net/psforever/services/galaxy/GalaxyAction.scala"
|
||||
- "src/main/scala/net/psforever/services/galaxy/GalaxyResponse.scala"
|
||||
- "src/main/scala/net/psforever/services/hart/HartEvent.scala"
|
||||
- "src/main/scala/net/psforever/services/hart/HartTimerActions.scala"
|
||||
- "src/main/scala/net/psforever/services/local/LocalAction.scala"
|
||||
- "src/main/scala/net/psforever/services/local/LocalResponse.scala"
|
||||
- "src/main/scala/net/psforever/services/vehicle/VehicleAction.scala"
|
||||
|
|
|
|||
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
52
build.sbt
52
build.sbt
|
|
@ -40,45 +40,47 @@ lazy val psforeverSettings = Seq(
|
|||
classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat,
|
||||
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
|
||||
libraryDependencies ++= Seq(
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.6.11",
|
||||
"com.typesafe.akka" %% "akka-slf4j" % "2.6.11",
|
||||
"com.typesafe.akka" %% "akka-protobuf-v3" % "2.6.11",
|
||||
"com.typesafe.akka" %% "akka-stream" % "2.6.11",
|
||||
"com.typesafe.akka" %% "akka-testkit" % "2.6.11" % "test",
|
||||
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.11",
|
||||
"com.typesafe.akka" %% "akka-cluster-typed" % "2.6.11",
|
||||
"com.typesafe.akka" %% "akka-coordination" % "2.6.11",
|
||||
"com.typesafe.akka" %% "akka-cluster-tools" % "2.6.11",
|
||||
"com.typesafe.akka" %% "akka-slf4j" % "2.6.11",
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-slf4j" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-protobuf-v3" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-stream" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-testkit" % "2.6.13" % "test",
|
||||
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-cluster-typed" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-coordination" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-cluster-tools" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-slf4j" % "2.6.13",
|
||||
"com.typesafe.akka" %% "akka-http" % "10.2.4",
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.3",
|
||||
"org.specs2" %% "specs2-core" % "4.10.6" % "test",
|
||||
"org.scalatest" %% "scalatest" % "3.2.3" % "test",
|
||||
"org.scalatest" %% "scalatest" % "3.2.6" % "test",
|
||||
"org.scodec" %% "scodec-core" % "1.11.7",
|
||||
"ch.qos.logback" % "logback-classic" % "1.2.3",
|
||||
"org.log4s" %% "log4s" % "1.9.0",
|
||||
"org.fusesource.jansi" % "jansi" % "2.1.1",
|
||||
"org.fusesource.jansi" % "jansi" % "2.3.2",
|
||||
"org.scoverage" %% "scalac-scoverage-plugin" % "1.4.2",
|
||||
"com.github.nscala-time" %% "nscala-time" % "2.26.0",
|
||||
"com.github.t3hnar" %% "scala-bcrypt" % "4.3.0",
|
||||
"org.scala-graph" %% "graph-core" % "1.13.2",
|
||||
"io.kamon" %% "kamon-bundle" % "2.1.10",
|
||||
"io.kamon" %% "kamon-apm-reporter" % "2.1.10",
|
||||
"org.json4s" %% "json4s-native" % "3.6.10",
|
||||
"io.getquill" %% "quill-jasync-postgres" % "3.6.0",
|
||||
"org.flywaydb" % "flyway-core" % "7.5.0",
|
||||
"org.postgresql" % "postgresql" % "42.2.18",
|
||||
"io.kamon" %% "kamon-bundle" % "2.1.13",
|
||||
"io.kamon" %% "kamon-apm-reporter" % "2.1.13",
|
||||
"org.json4s" %% "json4s-native" % "3.6.11",
|
||||
"io.getquill" %% "quill-jasync-postgres" % "3.7.0",
|
||||
"org.flywaydb" % "flyway-core" % "7.7.1",
|
||||
"org.postgresql" % "postgresql" % "42.2.19",
|
||||
"com.typesafe" % "config" % "1.4.1",
|
||||
"com.github.pureconfig" %% "pureconfig" % "0.14.0",
|
||||
"com.github.pureconfig" %% "pureconfig" % "0.14.1",
|
||||
"com.beachape" %% "enumeratum" % "1.6.1",
|
||||
"joda-time" % "joda-time" % "2.10.9",
|
||||
"joda-time" % "joda-time" % "2.10.10",
|
||||
"commons-io" % "commons-io" % "2.8.0",
|
||||
"com.github.scopt" %% "scopt" % "4.0.0",
|
||||
"io.sentry" % "sentry-logback" % "3.2.1",
|
||||
"com.github.scopt" %% "scopt" % "4.0.1",
|
||||
"io.sentry" % "sentry-logback" % "4.3.0",
|
||||
"io.circe" %% "circe-core" % "0.13.0",
|
||||
"io.circe" %% "circe-generic" % "0.13.0",
|
||||
"io.circe" %% "circe-parser" % "0.13.0",
|
||||
"org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.0",
|
||||
"org.bouncycastle" % "bcprov-jdk15on" % "1.68"
|
||||
"org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.1",
|
||||
"org.bouncycastle" % "bcprov-jdk15on" % "1.68",
|
||||
"org.codehaus.janino" % "janino" % "3.1.3"
|
||||
),
|
||||
// TODO(chord): remove exclusion when SessionActor is refactored: https://github.com/psforever/PSF-LoginServer/issues/279
|
||||
coverageExcludedPackages := "net\\.psforever\\.actors\\.session\\.SessionActor.*"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@
|
|||
{
|
||||
"groupName": "circe",
|
||||
"packagePatterns": "^io.circe"
|
||||
},
|
||||
{
|
||||
"groupName": "kamon",
|
||||
"packagePatterns": "^io.kamon"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ object SocketActor {
|
|||
socketActor ! toSocket(message)
|
||||
}
|
||||
} else {
|
||||
log.info("Network simulator dropped packet")
|
||||
log.trace("Network simulator dropped packet")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, _, _, _)) =>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)"))
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
30
src/main/scala/net/psforever/objects/geometry/Geometry.scala
Normal file
30
src/main/scala/net/psforever/objects/geometry/Geometry.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/main/scala/net/psforever/objects/geometry/GeometryForm.scala
Normal file
126
src/main/scala/net/psforever/objects/geometry/GeometryForm.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 => ;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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]()
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue