diff --git a/.github/workflows/comment-commands.yml b/.github/workflows/comment-commands.yml
new file mode 100644
index 0000000000..9a1bd3a1f0
--- /dev/null
+++ b/.github/workflows/comment-commands.yml
@@ -0,0 +1,21 @@
+name: Slash Command Dispatch
+on:
+ issue_comment:
+ types: [created]
+jobs:
+ slashCommandDispatch:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Slash Command Dispatch
+ id: scd
+ uses: peter-evans/slash-command-dispatch@v2
+ with:
+ token: ${{ secrets.SLASH_COMMANDS_PAT }}
+ issue-type: pull-request
+ dispatch-type: repository
+ permission: write
+ commands: |
+ test-server
+ static-args: |
+ repository=${{ github.repository }}
+ comment-id=${{ github.event.comment.id }}
\ No newline at end of file
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yml
similarity index 100%
rename from .github/workflows/publish.yaml
rename to .github/workflows/publish.yml
diff --git a/.github/workflows/test-server-command.yml b/.github/workflows/test-server-command.yml
new file mode 100644
index 0000000000..cbc68e62b1
--- /dev/null
+++ b/.github/workflows/test-server-command.yml
@@ -0,0 +1,38 @@
+name: Publish PR Docker Image
+on:
+ repository_dispatch:
+ types: [test-server-command]
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Dump the client payload context
+ env:
+ PAYLOAD_CONTEXT: ${{ toJson(github.event.client_payload) }}
+ run: echo "$PAYLOAD_CONTEXT"
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.client_payload.pull_request.head.sha }}
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v1
+ - name: Set variables
+ id: prep
+ run: |
+ DOCKER_IMAGE=test-server
+ REPOSITORY="$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')" # Repository name must be lowercase in image tags
+ TAGS="ghcr.io/${REPOSITORY}/${DOCKER_IMAGE}:${{ github.event.client_payload.pull_request.head.sha }}"
+
+ echo ::set-output name=tags::${TAGS}
+ - name: Log in to GitHub Docker Registry
+ uses: docker/login-action@v1
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Build container image
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.prep.outputs.tags }} #
\ No newline at end of file
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yml
similarity index 94%
rename from .github/workflows/test.yaml
rename to .github/workflows/test.yml
index bba53b3c66..6a7cef260f 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yml
@@ -1,9 +1,11 @@
name: Test
on:
push:
+ paths-ignore: .github\workflows
branches:
- master
pull_request:
+ paths-ignore: .github\workflows
types: [ opened, reopened, synchronize ]
jobs:
test:
diff --git a/build.sbt b/build.sbt
index af573b800d..a3a49613ba 100644
--- a/build.sbt
+++ b/build.sbt
@@ -72,7 +72,7 @@ lazy val psforeverSettings = Seq(
"com.github.pureconfig" %% "pureconfig" % "0.16.0",
"com.beachape" %% "enumeratum" % "1.6.1",
"joda-time" % "joda-time" % "2.10.10",
- "commons-io" % "commons-io" % "2.9.0",
+ "commons-io" % "commons-io" % "2.10.0",
"com.github.scopt" %% "scopt" % "4.0.1",
"io.sentry" % "sentry-logback" % "5.0.1",
"io.circe" %% "circe-core" % "0.14.1",
diff --git a/config/worldserver.ini b/config/worldserver.ini
new file mode 100644
index 0000000000..ae2b4f025c
--- /dev/null
+++ b/config/worldserver.ini
@@ -0,0 +1,259 @@
+#######################################
+# PSForever Server configuration file #
+#######################################
+
+###################################################################################################
+# EXAMPLE CONFIG
+#
+# Variable (type)
+# Description: Brief description what the variable is doing.
+# Important: Annotation for important things about this variable.
+# Example: "Example, i.e. if the value is a string"
+# Range: [0, 10] - (Disabled, Enabled)
+# Default: 10 - (Enabled|Comment|Variable name in case of grouped config options)
+# 0 - (Disabled|Comment|Variable name in case of grouped config options)
+#
+# Note to developers:
+# - Copy this example to keep the formatting.
+# - Line breaks should be at column 100.
+###################################################################################################
+
+###################################################################################################
+# DATABASE SETTINGS
+###################################################################################################
+
+[database]
+
+# Hostname (string)
+# Description: The hostname of the PostgreSQL server.
+# Important: Make sure your database isn't accessible outside of localhost without a
+# strong username and password!
+# Default: "localhost"
+
+Hostname = "localhost"
+
+# Port (int)
+# Description: The TCP port to connect to the database with.
+# Range: [1, 65535] - (TCP port 1, TCP port 65535)
+# Default: 5432 - (Connect to TCP port 5432 -- the default for PostgreSQL)
+
+Port = 5432
+
+# SSL (enum)
+# Description: The SSL configuration of the database connection.
+# Values: Disable - (Do not use SSL -- not recommended for public servers)
+# Prefer - (Try SSL first, but fallback to unencrypted)
+# Require - (Use SSL and fail if it is not enabled or configured correctly
+# Verify - (Use SSL and verify that the certificate is authentic)
+# Default: Prefer
+
+SSL = Prefer
+
+# Database (string)
+# Description: The database name to use on the SQL server.
+# Important: Make sure your username has been GRANTed ALL PRIVILEGES to the database.
+# See the setup documentation for instructions on achieving this.
+# Default: "psforever"
+
+Database = "psforever"
+
+# Username (string)
+# Description: The username to connect to the SQL server with.
+# Default: "psforever"
+
+Username = "psforever"
+
+# Password (string)
+# Description: The password to connect to the SQL server with.
+# Important: CHANGE THIS PASSWORD BEFORE RUNNING A PUBLIC SERVER.
+# Default: "psforever"
+
+Password = "psforever"
+
+###################################################################################################
+# WORLDSERVER SETTINGS
+###################################################################################################
+
+[worldserver]
+
+# ListeningPort (int)
+# Description: The UDP listening port for the worldserver.
+# Important: Must be different from the loginserver.ListeningPort. Ports below 1024 are
+# privileged on Linux and may require root.
+# Range: [1, 65535] - (UDP port 1, UDP port 65535)
+# Default: 51001 - (Listen on UDP port 51001)
+
+ListeningPort = 51001
+
+# Hostname (string)
+# Description: The server's public hostname or IP address. Required when the server is
+# behind a proxy or running in a container/virtual machine.
+# Important: If left as the default, the hostname will be the same as the primary
+# internet interface.
+# Default: ""
+
+Hostname = ""
+
+# ServerName (string)
+# Description: The name of the server as displayed in the server browser.
+# Range: Length = [1, 31]
+# Default: "PSForever"
+
+ServerName = "PSForever"
+
+# ServerType (enum)
+# Description: How the server is displayed in the server browser.
+# Values: Released, Beta, Development
+# Default: Released
+
+ServerType = Released
+
+###################################################################################################
+# LOGINSERVER SETTINGS
+###################################################################################################
+
+[loginserver]
+
+# ListeningPort (int)
+# Description: The UDP listening port for the loginserver.
+# Important: Must be different from the worldserver.ListeningPort. Ports below 1024 are
+# privileged on Linux and may require root.
+# Range: [1, 65535] - (UDP port 1, UDP port 65535)
+# Default: 51000 - (Listen on UDP port 51000)
+
+ListeningPort = 51000
+
+# CreateMissingAccounts (boolean)
+# Description: Account usernames that don't exist yet will be automatically created in the
+# database. Useful for test servers and development testing.
+# Important: Not recommended for production servers as typos are easy to make on
+# first registration. Use PSFPortal (https://github.com/psforever/PSFPortal)
+# instead.
+# Default: yes - (Enabled)
+# no - (Disabled)
+
+CreateMissingAccounts = yes
+
+###################################################################################################
+# PSADMIN SETTINGS
+###################################################################################################
+
+[psadmin]
+
+# ListeningPort (int)
+# Description: The TCP listening port for the server admin interface.
+# Important: Must be different from the worldserver and loginserver ListeningPort.
+# Ports below 1024 are privileged on Linux and may require root.
+# NEVER EXPOSE THIS PORT TO THE INTERNET! CHECK YOUR FIREWALL CONFIG.
+# Range: [1, 65535] - (TCP port 1, TCP port 65535)
+# Default: 51002 - (Listen on TCP port 51002)
+
+ListeningPort = 51002
+
+###################################################################################################
+# NETWORK SETTINGS
+###################################################################################################
+
+[network]
+
+# Session.InboundGraceTime (time)
+# Description: The maximum amount of time since the last inbound packet from a UDP session
+# before it is dropped.
+# Important: Lower values will cause legitimate clients to be dropped during loading
+# screens, but higher values will make the server be more susceptible to
+# denial of service attacks and running out of memory.
+# Range: [10 seconds, 10 minutes] - (10 second grace, 10 minute grace)
+# Default: 1 minute - (Clients sending a packet at least
+# once a minute stay alive)
+
+Session.InboundGraceTime = 1 minute
+
+# Session.OutboundGraceTime (time)
+# Description: The maximum amount of time since the last outbound packet for a UDP session
+# before it is dropped. Can be used as a watchdog for hung server sessions.
+# Important: Lower values will cause legitimate clients to be dropped during server
+# lag spikes or Zone transitions.
+# Range: [10 seconds, 10 minutes] - (10 second grace, 10 minute grace)
+# Default: 1 minute - (Clients receiving a packet at least
+# once a minute stay alive)
+
+Session.OutboundGraceTime = 1 minute
+
+###################################################################################################
+# DEVELOPER SETTINGS
+# - NETWORK SIMULATOR
+###################################################################################################
+
+[developer]
+
+###################################################################################################
+# NETWORK SIMULATOR
+#
+# NetSim.Active (boolean)
+# Description: Enable artificial packet unreliability. Used for development testing.
+# Active equally on upstream and downstream packets.
+# Important: DO NOT enable on servers with a high packet rate as buffering increases
+# the memory load of the server and it will (by design) affect performance.
+# Default: no - (Disabled)
+# yes - (Enabled)
+
+NetSim.Active = no
+
+# NetSim.Loss (float)
+# Description: The percentage of outgoing and incoming packets that are dropped.
+# Range: [0.0, 1.0] - (0% loss, 100% packet loss)
+# Default: 0.02 - (2% packet loss)
+
+NetSim.Loss = 0.0
+
+# NetSim.Delay (time)
+# Description: The time a packet is buffered before being delivered to simulate delay.
+# The artificial delay is in addition to any real network latency.
+# Important: Longer delays will lead to larger packet buffering, which may cause
+# out-of-memory errors depending on packet rate.
+# Range: [0, 2 seconds] - (No delay, 2 seconds delay)
+# Default: 150 milliseconds - (Packets lag for 150 milliseconds)
+
+NetSim.Delay = 150 milliseconds
+
+# NetSim.ReorderChance (float)
+# Description: The percentage chance that a packet will be ordered randomly in the delay
+# buffer. If the NetSim.Delay is too small then packets won't be reordered.
+# Range: [0.0, 1.0] - (No packet reordering, reorder every packet)
+# Default: 0.005 - (0.5% chance of a packet being reordered)
+
+NetSim.ReorderChance = 0.05
+
+# NetSim.ReorderTime (time)
+# Description: If a packet is reordered, the maximum time in the future or the past where
+# it will randomly appear.
+# Range: [0, 2 seconds] - (No packet reordering, randomly shift +/- 2 seconds)
+# Default: 150 milliseconds - (+/- 150 milliseconds)
+
+NetSim.ReorderTime = 150 milliseconds
+
+#
+###################################################################################################
+
+
+[kamon]
+
+# Kamon
+#
+# Active (boolean)
+# Description: Enables reporting of metrics to Kamon.io
+# Important: Ensure the API key is set in application.conf in the same
+# folder as PsLogin.scala
+# Default: no - (Disabled)
+# yes - (Enabled)
+
+Active = yes
+
+[antihack]
+
+# HitPositionDiscrepancyThreshold (int)
+# Description: The distance (squared) threshold that triggers if the reported hit location of a shot does not match the object being hit's location on the server
+# Range: [1, 1000000]
+# Default: 10000 (sqrt 10000 = ~100 ingame units)
+
+HitPositionDiscrepancyThreshold = 10000
\ No newline at end of file
diff --git a/pslogin/src/main/resources/user.conf b/pslogin/src/main/resources/user.conf
new file mode 100644
index 0000000000..6b27fc428c
--- /dev/null
+++ b/pslogin/src/main/resources/user.conf
@@ -0,0 +1,3 @@
+kamon {
+ apm.api-key = "3405691582g0h8776i44234htm"
+}
\ No newline at end of file
diff --git a/server/src/test/scala/actor/objects/PainboxTest.scala b/server/src/test/scala/actor/objects/PainboxTest.scala
new file mode 100644
index 0000000000..b545e35e0f
--- /dev/null
+++ b/server/src/test/scala/actor/objects/PainboxTest.scala
@@ -0,0 +1,202 @@
+package actor.objects
+
+import actor.base.FreedContextActorTest
+import actor.objects.PainboxControlTest.{CreatePlayer, RemovePlayer}
+import akka.actor.{ActorContext, ActorSystem, Props}
+import net.psforever.objects.Player.Respawn
+import net.psforever.objects.avatar.Avatar
+import net.psforever.objects.serverobject.doors.{Door, DoorControl}
+import net.psforever.objects.serverobject.painbox._
+import net.psforever.objects.serverobject.structures.{Building, StructureType}
+import net.psforever.objects.zones.{Zone, ZoneMap}
+import net.psforever.objects.{GlobalDefinitions, Player}
+import net.psforever.packet.game.UseItemMessage
+import net.psforever.types._
+import org.scalatest.BeforeAndAfterEach
+import org.specs2.mutable.Specification
+
+import scala.collection.concurrent.TrieMap
+import scala.concurrent.duration._
+
+class PainboxTest extends Specification {
+ "Painbox" should {
+ "Construct with all known painbox definitions" in {
+ Painbox(GlobalDefinitions.painbox)
+ Painbox(GlobalDefinitions.painbox_door_radius)
+ Painbox(GlobalDefinitions.painbox_door_radius_continuous)
+ Painbox(GlobalDefinitions.painbox_continuous)
+ Painbox(GlobalDefinitions.painbox_radius)
+ Painbox(GlobalDefinitions.painbox_radius_continuous)
+
+ ok
+ }
+
+ "Throw error with invalid object id" in {
+ val objectId = 1
+ new PainboxDefinition(objectId) must throwA[IllegalArgumentException](message = s"${objectId} is not a valid painbox object id")
+ }
+ }
+}
+
+class PainboxControlTest extends FreedContextActorTest with BeforeAndAfterEach {
+ val buildingMap = new TrieMap[Int, Building]()
+ val building = new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.amp_station)
+
+ building.Position = Vector3(1f, 1f, 1f)
+ building.Faction = PlanetSideEmpire.VS
+ buildingMap += building.GUID.guid -> building
+
+ // Initialize the zone so the SphereOfInfluence actor starts running
+ val zone = new Zone("test", new ZoneMap("map6"), 1) {
+ override def Buildings: Map[Int, Building] = { buildingMap.toMap }
+ }
+ zone.init(context)
+
+ val playerVS = CreatePlayer(zone, PlanetSideEmpire.VS, avatarId = 0)
+ val playerTR = CreatePlayer(zone, PlanetSideEmpire.TR, avatarId = 1)
+ val playerNC = CreatePlayer(zone, PlanetSideEmpire.NC, avatarId = 2)
+
+ override protected def beforeEach() : Unit = {
+ // Spawn players
+ playerVS.Spawn
+ playerVS.Position = Vector3(4f, 4f, 4f)
+
+ playerTR.Spawn
+ playerTR.Position = Vector3(4f, 4f, 4f)
+
+ playerNC.Spawn
+ playerNC.Position = Vector3(4f, 4f, 4f)
+ }
+
+ override protected def afterEach() : Unit = {
+ // Kill players
+ playerVS.Die
+ playerTR.Die
+ playerNC.Die
+ }
+
+ "PainboxControl" should {
+ "Damage hostile player if dependent door is open" in {
+ val door = Door(GlobalDefinitions.door)
+ door.Actor = system.actorOf(Props(classOf[DoorControl], door), "door")
+ door.Owner = building
+ door.Position = Vector3(3f, 3f, 3f)
+ building.Amenities = door
+
+ val painbox = Painbox(GlobalDefinitions.painbox_door_radius)
+ painbox.Actor = system.actorOf(Props(classOf[PainboxControl], painbox), "painbox")
+ painbox.Owner = building
+ painbox.Position = Vector3(2f, 2f, 2f)
+ building.Amenities = painbox
+
+ painbox.Actor ! Painbox.Start()
+
+
+
+ // Ensure SOI actor has picked up spawned players
+ awaitAssert(() -> {
+ assert(building.PlayersInSOI.length == 3)
+ }, 6 seconds)
+
+ assert(playerVS.Health == 100)
+ assert(playerTR.Health == 100)
+ assert(playerNC.Health == 100)
+
+ door.Open = Some(playerVS) // Open the door
+
+ painbox.Actor ! "startup"
+ expectNoMessage(500 milliseconds)
+ assert(playerVS.Health == 100)
+ assert(playerTR.Health < 100)
+ assert(playerNC.Health < 100)
+ }
+
+ "Not damage hostile player is dependent door is closed" in {
+
+ }
+
+ "Not rely on nearby door state if not dependent on door (open)" in {
+// val (player, painbox, door) = PainboxControlTest.SetUpAgents(context, PlanetSideEmpire.NC, GlobalDefinitions.painbox_continuous)
+//
+// val originalHealth = player.Health
+// door.Open = Some(player) // Open the door
+//
+// painbox.Actor ! "startup"
+// expectNoMessage(500 milliseconds)
+// assert(player.Health < originalHealth)
+//
+// door.Open = None // Close the door
+// expectNoMessage(500 milliseconds)
+// assert(player.Health < originalHealth)
+ }
+
+ "Not damage friendly players" in {
+
+ }
+
+ "Not damage player outside range" in {
+
+ }
+
+ "Not damage player if Owner faction is NEUTRAL" in {
+
+ }
+
+ "Throw error if no SOI is defined on Owner" in {
+
+ }
+
+ "Damage once per second at most" in {
+
+ }
+ }
+}
+
+object PainboxControlTest {
+ def CreatePlayer(zone: Zone, faction: PlanetSideEmpire.Value, avatarId: Int): Player = {
+ val player = Player(Avatar(avatarId, s"test-${faction.toString}", faction, CharacterSex.Male, 0, CharacterVoice.Mute))
+ zone.Population ! Zone.Population.Join(player.avatar)
+ zone.Population ! Zone.Population.Spawn(player.avatar, player, null)
+
+ player
+ }
+
+ def RemovePlayer(zone: Zone, player: Player): Unit = {
+ zone.Population ! Zone.Population.Leave(player.avatar)
+ }
+
+ def SetUpAgents(context: ActorContext, faction : PlanetSideEmpire.Value, definition : PainboxDefinition)(implicit system : ActorSystem) : (Player, Painbox, Door) = {
+
+ // Initialize the zone so the SphereOfInfluence actor starts running
+ val zone = new Zone("test", new ZoneMap("map6"), 1)
+ zone.init(context)
+// zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-zone")
+// zone.Actor ! Zone.Init()
+
+ val avatar = Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute)
+ val player = Player(avatar)
+ player.Spawn
+ player.Position = Vector3(4f, 4f, 4f)
+ zone.Population ! Zone.Population.Join(avatar)
+
+ val building = new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.amp_station)
+
+ building.Position = Vector3(1f, 1f, 1f)
+ building.Faction = PlanetSideEmpire.VS
+
+ Thread.sleep(5000) // Give SOI actor time to repopulate
+
+ val painbox = Painbox(definition)
+ painbox.Actor = system.actorOf(Props(classOf[PainboxControl], painbox), "painbox")
+ painbox.Owner = building
+ painbox.Position = Vector3(2f, 2f, 2f)
+
+ val door = Door(GlobalDefinitions.door)
+ door.Actor = system.actorOf(Props(classOf[DoorControl], door), "door")
+ door.Owner = building
+ door.Position = Vector3(3f, 3f, 3f)
+
+ (player, painbox, door)
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/scala/net/psforever/types/LatticeCavernBenefits.scala b/src/main/scala/net/psforever/types/LatticeCavernBenefits.scala
new file mode 100644
index 0000000000..ae44a15f6e
--- /dev/null
+++ b/src/main/scala/net/psforever/types/LatticeCavernBenefits.scala
@@ -0,0 +1,23 @@
+package net.psforever.types
+
+/*
+ * Cavern benefits: (stackable)
+ * 000 - None
+ * 004 - Speed Module
+ * 008 - Shield Module
+ * 016 - Vehicle Module
+ * 032 - Equipment Module
+ * 064 - Health Module
+ * 128 - Pain Module
+ */
+object LatticeCavernBenefits extends Enumeration {
+ type Type = Int
+
+ val None = Value(0)
+ val Speed = Value(4)
+ val Shield = Value(8)
+ val Vehicle = Value(16)
+ val Equipment = Value(32)
+ val Health = Value(64)
+ val Pain = Value(128)
+}
diff --git a/src/test/scala/objects/terminal/CaptureTerminalTest.scala b/src/test/scala/objects/terminal/CaptureTerminalTest.scala
new file mode 100644
index 0000000000..0b2ea4cebc
--- /dev/null
+++ b/src/test/scala/objects/terminal/CaptureTerminalTest.scala
@@ -0,0 +1,89 @@
+//package objects.terminal
+//
+//import akka.actor.{ActorRef, ActorSystem, Props}
+//import base.ActorTest
+//import net.psforever.objects.serverobject.CommonMessages
+//import net.psforever.objects.serverobject.structures.{Building, StructureType}
+//import net.psforever.objects.serverobject.terminals.{CaptureTerminal, CaptureTerminalControl, CaptureTerminalDefinition}
+//import net.psforever.objects.zones.Zone
+//import net.psforever.objects.{Avatar, GlobalDefinitions, Player}
+//import net.psforever.packet.game.PlanetSideGUID
+//import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire}
+//
+//class CaptureTerminalTest extends ActorTest {
+// "CaptureTerminal" should {
+// "construct" in {
+// val terminal = CaptureTerminal(GlobalDefinitions.capture_terminal)
+// terminal.Actor = system.actorOf(Props(classOf[CaptureTerminalControl], terminal), "capture-terminal-control")
+// assert(terminal.Actor != ActorRef.noSender)
+// }
+//
+// "fail on invalid object type" in {
+// assertThrows[IllegalArgumentException] {
+// CaptureTerminal(new CaptureTerminalDefinition(0))
+// }
+// }
+//
+// "can be hacked" in {
+// val (player, terminal) = CaptureTerminalTest.SetUpAgents(PlanetSideEmpire.TR, PlanetSideEmpire.VS, 1)
+// assert(terminal.HackedBy.isEmpty)
+//
+// terminal.Actor ! CommonMessages.Hack(player)
+// expectMsg(true)
+// assert(terminal.HackedBy.nonEmpty)
+// assert(terminal.HackedBy.get._1.Faction == PlanetSideEmpire.VS)
+// }
+//
+// "can be resecured" in {
+// val (player, player2, terminal) = CaptureTerminalTest.SetUpAgents(PlanetSideEmpire.TR, PlanetSideEmpire.VS, PlanetSideEmpire.TR, 2)
+// assert(terminal.HackedBy.isEmpty)
+//
+// terminal.Actor ! CommonMessages.Hack(player)
+// expectMsg(true)
+// assert(terminal.HackedBy.nonEmpty)
+// assert(terminal.HackedBy.get._1.Faction == PlanetSideEmpire.VS)
+//
+// terminal.Actor ! CommonMessages.ClearHack()
+// Thread.sleep(500L) // blocking
+// assert(terminal.HackedBy.isEmpty)
+// }
+//
+// "can overwrite hack" in {
+// val (player, player2, terminal) = CaptureTerminalTest.SetUpAgents(PlanetSideEmpire.TR, PlanetSideEmpire.VS, PlanetSideEmpire.NC, 3)
+// assert(terminal.HackedBy.isEmpty)
+//
+// terminal.Actor ! CommonMessages.Hack(player)
+// expectMsg(true)
+// assert(terminal.HackedBy.nonEmpty)
+// assert(terminal.HackedBy.get._1.Faction == PlanetSideEmpire.VS)
+//
+// terminal.Actor ! CommonMessages.Hack(player2)
+// expectMsg(true)
+// assert(terminal.HackedBy.nonEmpty)
+// assert(terminal.HackedBy.get._1.Faction == PlanetSideEmpire.NC)
+// }
+// }
+//}
+//
+//object CaptureTerminalTest {
+// def SetUpAgents(ccFaction : PlanetSideEmpire.Value, playerFaction : PlanetSideEmpire.Value, player2Faction: PlanetSideEmpire.Value, actorId : Int)(implicit system : ActorSystem) : (Player, Player, CaptureTerminal) = {
+// val (player, terminal) = SetUpAgents(ccFaction, playerFaction, actorId)
+//
+// val player2 = new Player(Avatar("test", player2Faction, CharacterGender.Male, 0, CharacterVoice.Mute))
+// player2.GUID = PlanetSideGUID(1)
+//
+// (player, player2, terminal)
+// }
+//
+// def SetUpAgents(ccFaction : PlanetSideEmpire.Value, playerFaction : PlanetSideEmpire.Value, actorId: Int)(implicit system : ActorSystem) : (Player, CaptureTerminal) = {
+// val terminal = CaptureTerminal(GlobalDefinitions.capture_terminal)
+// terminal.Actor = system.actorOf(Props(classOf[CaptureTerminalControl], terminal), s"capture-terminal-control-${actorId}")
+// terminal.Owner = new Building(guid = 0, map_id = 0, Zone.Nowhere, StructureType.Building)
+// terminal.Owner.Faction = ccFaction
+//
+// val player = new Player(Avatar("test", playerFaction, CharacterGender.Male, 0, CharacterVoice.Mute))
+// player.GUID = PlanetSideGUID(1)
+//
+// (player, terminal)
+// }
+//}
\ No newline at end of file