diff --git a/.gitignore b/.gitignore index 7a0607ad..18755ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ logs/ *.exe *.zip pscrypto-lib/ +.bsp/ +project/project/ diff --git a/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala b/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala new file mode 100644 index 00000000..7da3afee --- /dev/null +++ b/server/src/test/scala/actor/objects/AutoRepairIntegrationTest.scala @@ -0,0 +1,554 @@ +// Copyright (c) 2020 PSForever +package actor.objects + +import akka.actor.Props +import akka.testkit.TestProbe +import base.FreedContextActorTest +import net.psforever.actors.zone.BuildingActor +import net.psforever.objects.avatar.Avatar +import net.psforever.objects.ballistics.{Projectile, SourceEntry} +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.MaxNumberSource +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.deploy.Deployment +import net.psforever.objects.serverobject.resourcesilo.{ResourceSilo, ResourceSiloControl} +import net.psforever.objects.serverobject.structures.{AutoRepairStats, Building, StructureType} +import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal, TerminalControl} +import net.psforever.objects.vehicles.VehicleControl +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.DamageResolution +import net.psforever.objects.vital.damage.DamageProfile +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.vital.projectile.ProjectileReason +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle} +import net.psforever.services.galaxy.GalaxyService +import net.psforever.services.{InterstellarClusterService, ServiceManager} +import net.psforever.types._ + +import scala.collection.concurrent.TrieMap +import scala.concurrent.duration._ + +class AutoRepairFacilityIntegrationTest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + expectNoMessage(1000 milliseconds) + val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) + val avatarProbe = new TestProbe(system) + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + override def AvatarEvents = avatarProbe.ref + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + } + 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)) + player.Spawn() + val weapon = new Tool(GlobalDefinitions.suppressor) + val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition) + val silo = new ResourceSilo() + guid.register(player, number = 1) + guid.register(weapon, number = 2) + guid.register(weapon.AmmoSlot.Box, number = 3) + guid.register(terminal, number = 4) + guid.register(silo, number = 5) + guid.register(building, number = 6) + + building.Amenities = silo + building.Amenities = terminal + terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal") + silo.NtuCapacitor = 1000 + silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") + silo.Actor ! "startup" + + val wep_fmode = weapon.FireMode + val wep_prof = wep_fmode.Add + val proj = weapon.Projectile + val proj_prof = proj.asInstanceOf[DamageProfile] + val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero) + val resolved = DamageInteraction( + SourceEntry(terminal), + ProjectileReason( + DamageResolution.Hit, + projectile, + terminal.DamageModel + ), + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.calculate() + + "AutoRepair" should { + "should activate on damage and trade NTU from the facility's resource silo for repairs" in { + assert(silo.NtuCapacitor == silo.MaxNtuCapacitor) + assert(terminal.Health == terminal.MaxHealth) + terminal.Actor ! Vitality.Damage(applyDamageTo) + + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + assert(terminal.Health < terminal.MaxHealth) + var i = 0 //safety counter + while(terminal.Health < terminal.MaxHealth && i < 100) { + i += 1 + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + } + assert(silo.NtuCapacitor < silo.MaxNtuCapacitor) + assert(terminal.Health == terminal.MaxHealth) + } + } +} + +class AutoRepairFacilityIntegrationGiveNtuTest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + expectNoMessage(1000 milliseconds) + val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) + val avatarProbe = new TestProbe(system) + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + override def AvatarEvents = avatarProbe.ref + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + } + val building = Building.Structure(StructureType.Facility)(name = "integ-fac-test-building", guid = 6, map_id = 0, zone, context) + building.Invalidate() + + val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition) + val silo = new ResourceSilo() + guid.register(terminal, number = 4) + guid.register(silo, number = 5) + guid.register(building, number = 6) + + building.Amenities = silo + building.Amenities = terminal + terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal") + terminal.Health = 0 + terminal.Destroyed = true + silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") + silo.Actor ! "startup" + + "AutoRepair" should { + "should activate and trade NTU frpom the silo only when NTU is made available" in { + assert(silo.NtuCapacitor == 0) + assert(terminal.Health == 0) + assert(terminal.Destroyed) + avatarProbe.expectNoMessage(max = 1000 milliseconds) //nothing + silo.Actor ! ResourceSilo.UpdateChargeLevel(1000) //then ... + + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + assert(terminal.Health < terminal.MaxHealth) + var i = 0 //safety counter + while(terminal.Health < terminal.MaxHealth && i < 1000) { + i += 1 + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + } + assert(silo.NtuCapacitor > 0 && silo.NtuCapacitor < silo.MaxNtuCapacitor) + assert(terminal.Health == terminal.MaxHealth) + assert(!terminal.Destroyed) + } + } +} + +class AutoRepairFacilityIntegrationAntGiveNtuTest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + expectNoMessage(1000 milliseconds) + var buildingMap = new TrieMap[Int, Building]() + val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val ant = Vehicle(GlobalDefinitions.ant) + val terminal = new Terminal(AutoRepairIntegrationTest.slow_terminal_definition) + val silo = new ResourceSilo() + val avatarProbe = new TestProbe(system) + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test-map"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + override def AvatarEvents = avatarProbe.ref + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + override def Vehicles = List(ant) + override def Buildings = { buildingMap.toMap } + } + val building = new Building( + name = "integ-fac-test-building", + building_guid = 6, + map_id = 0, + zone, + StructureType.Facility, + GlobalDefinitions.cryo_facility + ) + buildingMap += 6 -> building + building.Actor = context.spawn(BuildingActor(zone, building), "integ-fac-test-building-control").toClassic + building.Invalidate() + + guid.register(player, number = 1) + guid.register(ant, number = 2) + guid.register(terminal, number = 4) + guid.register(silo, number = 5) + guid.register(building, number = 6) + + val maxNtuCap = ant.Definition.MaxNtuCapacitor + player.Spawn() + ant.NtuCapacitor = maxNtuCap + ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") + ant.Zone = zone + ant.Seats(0).Occupant = player + ant.DeploymentState = DriveState.Deployed + building.Amenities = terminal + building.Amenities = silo + terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal") + terminal.Health = 0 + terminal.Destroyed = true + silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") + silo.Actor ! "startup" + + "AutoRepair" should { + "should activate and trade NTU from the silo only when NTU is made available from an ANT" in { + assert(silo.NtuCapacitor == 0) + assert(ant.NtuCapacitor == maxNtuCap) + assert(terminal.Health == 0) + assert(terminal.Destroyed) + avatarProbe.expectNoMessage(max = 1000 milliseconds) //nothing + silo.Actor ! CommonMessages.Use(player) //then ... + + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + assert(terminal.Health < terminal.MaxHealth) + var i = 0 //safety counter + while(terminal.Health < terminal.MaxHealth && i < 1000) { + i += 1 + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + } + assert(silo.NtuCapacitor > 0 && silo.NtuCapacitor < silo.MaxNtuCapacitor) + val ntuAfterRepairs = ant.NtuCapacitor + assert(ntuAfterRepairs < maxNtuCap) + assert(terminal.Health == terminal.MaxHealth) + assert(!terminal.Destroyed) + if(silo.NtuCapacitor < maxNtuCap) { + var j = 0 //safety counter + while(silo.NtuCapacitor < silo.MaxNtuCapacitor && j < 1000) { + j += 1 + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + } + } + assert(silo.NtuCapacitor == silo.MaxNtuCapacitor) + assert(ant.NtuCapacitor < ntuAfterRepairs) + println(s"Test '${testNames.head}' successful.") + } + } +} + +class AutoRepairFacilityIntegrationTerminalDestroyedTerminalAntTest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + expectNoMessage(1000 milliseconds) + var buildingMap = new TrieMap[Int, Building]() + val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val weapon = new Tool(GlobalDefinitions.suppressor) + val ant = Vehicle(GlobalDefinitions.ant) + val terminal = new Terminal(AutoRepairIntegrationTest.slow_terminal_definition) + val silo = new ResourceSilo() + val avatarProbe = new TestProbe(system) + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test-map"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + override def AvatarEvents = avatarProbe.ref + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + override def Vehicles = List(ant) + override def Buildings = { buildingMap.toMap } + } + val building = new Building( + name = "integ-fac-test-building", + building_guid = 6, + map_id = 0, + zone, + StructureType.Facility, + GlobalDefinitions.cryo_facility + ) + buildingMap += 6 -> building + building.Actor = context.spawn(BuildingActor(zone, building), "integ-fac-test-building-control").toClassic + building.Invalidate() + + guid.register(player, number = 1) + guid.register(ant, number = 2) + guid.register(weapon, number = 3) + guid.register(terminal, number = 4) + guid.register(silo, number = 5) + guid.register(building, number = 6) + guid.register(weapon.AmmoSlot.Box, number = 7) + + val maxNtuCap = ant.Definition.MaxNtuCapacitor + player.Spawn() + ant.NtuCapacitor = maxNtuCap + ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") + ant.Zone = zone + ant.Seats(0).Occupant = player + ant.DeploymentState = DriveState.Deployed + building.Amenities = terminal + building.Amenities = silo + terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal") + terminal.Health = 1 //not yet destroyed, but one shot away from it + silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") + silo.Actor ! "startup" + + val wep_fmode = weapon.FireMode + val wep_prof = wep_fmode.Add + val proj = weapon.Projectile + val proj_prof = proj.asInstanceOf[DamageProfile] + val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero) + val resolved = DamageInteraction( + SourceEntry(terminal), + ProjectileReason( + DamageResolution.Hit, + projectile, + terminal.DamageModel + ), + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.calculate() + + "AutoRepair" should { + "should activate upon destruction and trade NTU from the silo only when NTU is made available from an ANT" in { + assert(silo.NtuCapacitor == 0) + assert(ant.NtuCapacitor == maxNtuCap) + assert(!terminal.Destroyed) + avatarProbe.expectNoMessage(max = 1000 milliseconds) //nothing + terminal.Actor ! Vitality.Damage(applyDamageTo) + while(avatarProbe.receiveOne(max = 1000 milliseconds) != null) { /* health loss event(s) + state updates */ } + assert(terminal.Destroyed) + avatarProbe.expectNoMessage(max = 1000 milliseconds) //nothing + silo.Actor ! CommonMessages.Use(player) //then ... + + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + assert(terminal.Health < terminal.MaxHealth) + var i = 0 //safety counter + while(terminal.Health < terminal.MaxHealth && i < 1000) { + i += 1 + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + } + assert(silo.NtuCapacitor > 0 && silo.NtuCapacitor <= silo.MaxNtuCapacitor) + assert(ant.NtuCapacitor < maxNtuCap) + assert(terminal.Health == terminal.MaxHealth) + assert(!terminal.Destroyed) + println(s"Test '${testNames.head}' successful.") + } + } +} + +class AutoRepairFacilityIntegrationTerminalIncompleteRepairTest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + expectNoMessage(1000 milliseconds) + var buildingMap = new TrieMap[Int, Building]() + val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) + val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val weapon = new Tool(GlobalDefinitions.suppressor) + val ant = Vehicle(GlobalDefinitions.ant) + val terminal = new Terminal(AutoRepairIntegrationTest.slow_terminal_definition) + val silo = new ResourceSilo() + val avatarProbe = new TestProbe(system) + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test-map"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + override def AvatarEvents = avatarProbe.ref + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + override def Vehicles = List(ant) + override def Buildings = { buildingMap.toMap } + } + val building = new Building( + name = "integ-fac-test-building", + building_guid = 6, + map_id = 0, + zone, + StructureType.Facility, + GlobalDefinitions.cryo_facility + ) + buildingMap += 6 -> building + building.Actor = context.spawn(BuildingActor(zone, building), "integ-fac-test-building-control").toClassic + building.Invalidate() + + guid.register(player, number = 1) + guid.register(ant, number = 2) + guid.register(weapon, number = 3) + guid.register(terminal, number = 4) + guid.register(silo, number = 5) + guid.register(building, number = 6) + guid.register(weapon.AmmoSlot.Box, number = 7) + + val maxNtuCap = ant.Definition.MaxNtuCapacitor + player.Spawn() + ant.NtuCapacitor = maxNtuCap + ant.Actor = context.actorOf(Props(classOf[VehicleControl], ant), name = "test-ant") + ant.Zone = zone + ant.Seats(0).Occupant = player + ant.DeploymentState = DriveState.Deployed + building.Amenities = terminal + building.Amenities = silo + terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal") + terminal.Health = 1 //not yet destroyed, but one shot away from it + silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") + silo.Actor ! "startup" + + val wep_fmode = weapon.FireMode + val wep_prof = wep_fmode.Add + val proj = weapon.Projectile + val proj_prof = proj.asInstanceOf[DamageProfile] + val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero) + val resolved = DamageInteraction( + SourceEntry(terminal), + ProjectileReason( + DamageResolution.Hit, + projectile, + terminal.DamageModel + ), + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.calculate() + + "AutoRepair" should { + "should activate and trade NTU from the silo; if the ANT stops depositing, auto-repair continues" in { + assert(silo.NtuCapacitor == 0) + assert(ant.NtuCapacitor == maxNtuCap) + assert(!terminal.Destroyed) + avatarProbe.expectNoMessage(max = 1000 milliseconds) //nothing + terminal.Actor ! Vitality.Damage(applyDamageTo) + while(avatarProbe.receiveOne(max = 1000 milliseconds) != null) { /* health loss event(s) + state updates */ } + assert(terminal.Destroyed) + avatarProbe.expectNoMessage(max = 1000 milliseconds) //nothing + silo.Actor ! CommonMessages.Use(player) //then ... + + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + assert(terminal.Health < terminal.MaxHealth) + var i = 0 //safety counter + while(terminal.Health < terminal.MaxHealth && i < 10) { + i += 1 + avatarProbe.receiveOne(max = 1000 milliseconds) //some health update events ... + } + ant.Actor ! Deployment.TryUndeploy(DriveState.Undeploying) + ant.Actor ! Deployment.TryUndeploy(DriveState.Mobile) + while( avatarProbe.receiveOne(max = 1000 milliseconds) != null ) { /* remainder of the messages */ } + val siloCapacitor = silo.NtuCapacitor + val antCapacitor = ant.NtuCapacitor + val termHealth = terminal.Health + assert(ant.DeploymentState == DriveState.Mobile) + assert(siloCapacitor > 0 && siloCapacitor < silo.MaxNtuCapacitor) + assert(antCapacitor > 0 && antCapacitor < maxNtuCap) + assert(termHealth > 0 && termHealth < terminal.MaxHealth) + while(terminal.Health < terminal.MaxHealth && i < 20) { + i += 1 + avatarProbe.receiveOne(max = 1000 milliseconds) //some health update events ... + } + //while( avatarProbe.receiveOne(max = 1000 milliseconds) != null ) { /* remainder of the messages */ } + assert(siloCapacitor != silo.NtuCapacitor) //changing ... + assert(antCapacitor == ant.NtuCapacitor) //not supplying anymore + assert(terminal.Health > termHealth && terminal.Health <= terminal.MaxHealth) //still auto-repairing + println(s"Test '${testNames.head}' successful.") + } + } +} + +class AutoRepairTowerIntegrationTest extends FreedContextActorTest { + import akka.actor.typed.scaladsl.adapter._ + system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) + ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") + expectNoMessage(1000 milliseconds) + val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) + val avatarProbe = new TestProbe(system) + val catchall = new TestProbe(system).ref + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + override def AvatarEvents = avatarProbe.ref + override def LocalEvents = catchall + override def VehicleEvents = catchall + override def Activity = catchall + } + 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)) + player.Spawn() + val weapon = new Tool(GlobalDefinitions.suppressor) + val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition) + terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal") + guid.register(player, number = 1) + guid.register(weapon, number = 2) + guid.register(weapon.AmmoSlot.Box, number = 3) + guid.register(terminal, number = 4) + guid.register(building, number = 6) + + building.Amenities = terminal + building.Actor ! BuildingActor.SuppliedWithNtu() //artificial + building.Actor ! BuildingActor.PowerOn() //artificial + + val wep_fmode = weapon.FireMode + val wep_prof = wep_fmode.Add + val proj = weapon.Projectile + val proj_prof = proj.asInstanceOf[DamageProfile] + val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero) + val resolved = DamageInteraction( + SourceEntry(terminal), + ProjectileReason( + DamageResolution.Hit, + projectile, + terminal.DamageModel + ), + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.calculate() + + "AutoRepair" should { + "should activate on damage and trade NTU from the tower for repairs" in { + assert(terminal.Health == terminal.MaxHealth) + terminal.Actor ! Vitality.Damage(applyDamageTo) + + avatarProbe.receiveOne(max = 500 milliseconds) //health update event + assert(terminal.Health < terminal.MaxHealth) + var i = 0 //safety counter + while(terminal.Health < terminal.MaxHealth && i < 100) { + i += 1 + avatarProbe.receiveOne(max = 1000 milliseconds) //health update event + } + assert(terminal.Health == terminal.MaxHealth) + } + } +} + +object AutoRepairIntegrationTest { + val terminal_definition = new OrderTerminalDefinition(objId = 612) { + Name = "order_terminal" + MaxHealth = 500 + Damageable = true + Repairable = true + autoRepair = AutoRepairStats(200, 500, 500, 1) + RepairIfDestroyed = true + } + + val slow_terminal_definition = new OrderTerminalDefinition(objId = 612) { + Name = "order_terminal" + MaxHealth = 500 + Damageable = true + Repairable = true + autoRepair = AutoRepairStats(5, 500, 500, 1) + RepairIfDestroyed = true + } +} diff --git a/src/test/scala/objects/AutoRepairTest.scala b/server/src/test/scala/actor/objects/AutoRepairTest.scala similarity index 96% rename from src/test/scala/objects/AutoRepairTest.scala rename to server/src/test/scala/actor/objects/AutoRepairTest.scala index 5f137407..3e9b3246 100644 --- a/src/test/scala/objects/AutoRepairTest.scala +++ b/server/src/test/scala/actor/objects/AutoRepairTest.scala @@ -1,5 +1,5 @@ // Copyright (c) 2020 PSForever -package objects +package actor.objects import akka.actor.Props import akka.testkit.TestProbe @@ -78,10 +78,8 @@ class AutoRepairRequestNtuTest extends FreedContextActorTest { assert(terminal.Health < terminal.MaxHealth) val buildingMsg = buildingProbe.receiveOne(max = 600 milliseconds) assert(buildingMsg match { - case BuildingActor.Ntu(NtuCommand.Request(drain, _)) => - drain == terminal.Definition.autoRepair.get.drain - case _ => - false + case BuildingActor.Ntu(NtuCommand.Request(_, _)) => true + case _ => false }) } } @@ -142,11 +140,10 @@ class AutoRepairRequestNtuRepeatTest extends FreedContextActorTest { (0 to 3).foreach { _ => val buildingMsg = buildingProbe.receiveOne(max = 1000 milliseconds) assert(buildingMsg match { - case BuildingActor.Ntu(NtuCommand.Request(drain, _)) => - drain == terminal.Definition.autoRepair.get.drain - case _ => - false + case BuildingActor.Ntu(NtuCommand.Request(_, _)) => true + case _ => false }) + terminal.Actor ! NtuCommand.Grant(null, 0) } } } @@ -268,10 +265,8 @@ class AutoRepairRestoreRequestNtuTest extends FreedContextActorTest { terminal.Actor ! BuildingActor.SuppliedWithNtu() val buildingMsg = buildingProbe.receiveOne(max = 600 milliseconds) assert(buildingMsg match { - case BuildingActor.Ntu(NtuCommand.Request(drain, _)) => - drain == terminal.Definition.autoRepair.get.drain - case _ => - false + case BuildingActor.Ntu(NtuCommand.Request(_, _)) => true + case _ => false }) } } diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index a07287e6..1f5f3434 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -76,6 +76,12 @@ game { # Command experience rate cep-rate = 1.0 + # Modify the amount of mending per autorepair tick for facility amenities + amenity-autorepair-rate = 1.0 + + # Modify the amount of NTU drain per autorepair tick for facility amenities + amenity-autorepair-drain-rate = 0.5 + new-avatar { # Starting battle rank br = 1 diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index 57808839..8f1abea7 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -5,7 +5,7 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} import akka.{actor => classic} import net.psforever.actors.commands.NtuCommand -import net.psforever.objects.{CommonNtuContainer, NtuContainer} +import net.psforever.objects.NtuContainer import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} @@ -449,7 +449,7 @@ class BuildingActor( Behaviors.same case _ => //all other facilities require a storage silo for ntu - building.Amenities.find(_.isInstanceOf[NtuContainer]) match { + building.NtuSource match { case Some(ntuContainer) => ntuContainer.Actor ! msg //needs to redirect Behaviors.same @@ -466,8 +466,9 @@ class BuildingActor( class FakeNtuSource(private val building: Building) extends PlanetSideServerObject - with CommonNtuContainer { + with NtuContainer { override def NtuCapacitor = Float.MaxValue + override def NtuCapacitor_=(a: Float) = Float.MaxValue override def Faction = building.Faction override def Zone = building.Zone override def Definition = null diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 2c67790d..18d12666 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -7279,7 +7279,7 @@ object GlobalDefinitions { order_terminal.MaxHealth = 500 order_terminal.Damageable = true order_terminal.Repairable = true - order_terminal.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + order_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f order_terminal.RepairIfDestroyed = true order_terminal.Subtract.Damage1 = 8 @@ -7343,7 +7343,7 @@ object GlobalDefinitions { cert_terminal.MaxHealth = 500 cert_terminal.Damageable = true cert_terminal.Repairable = true - cert_terminal.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + cert_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f cert_terminal.RepairIfDestroyed = true cert_terminal.Subtract.Damage1 = 8 @@ -7351,7 +7351,7 @@ object GlobalDefinitions { implant_terminal_mech.MaxHealth = 1500 //TODO 1000; right now, 1000 (mech) + 500 (interface) implant_terminal_mech.Damageable = true implant_terminal_mech.Repairable = true - implant_terminal_mech.autoRepair = AutoRepairStats(1, 5000, 2400, 0.5f) + implant_terminal_mech.autoRepair = AutoRepairStats(1.6f, 5000, 2400, 0.5f) //ori. 1, 5000, 2400, 0.5f implant_terminal_mech.RepairIfDestroyed = true implant_terminal_interface.Name = "implant_terminal_interface" @@ -7371,7 +7371,7 @@ object GlobalDefinitions { ground_vehicle_terminal.MaxHealth = 500 ground_vehicle_terminal.Damageable = true ground_vehicle_terminal.Repairable = true - ground_vehicle_terminal.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + ground_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f ground_vehicle_terminal.RepairIfDestroyed = true ground_vehicle_terminal.Subtract.Damage1 = 8 @@ -7384,7 +7384,7 @@ object GlobalDefinitions { air_vehicle_terminal.MaxHealth = 500 air_vehicle_terminal.Damageable = true air_vehicle_terminal.Repairable = true - air_vehicle_terminal.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + air_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f air_vehicle_terminal.RepairIfDestroyed = true air_vehicle_terminal.Subtract.Damage1 = 8 @@ -7397,7 +7397,7 @@ object GlobalDefinitions { dropship_vehicle_terminal.MaxHealth = 500 dropship_vehicle_terminal.Damageable = true dropship_vehicle_terminal.Repairable = true - dropship_vehicle_terminal.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + dropship_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f dropship_vehicle_terminal.RepairIfDestroyed = true dropship_vehicle_terminal.Subtract.Damage1 = 8 @@ -7410,7 +7410,7 @@ object GlobalDefinitions { vehicle_terminal_combined.MaxHealth = 500 vehicle_terminal_combined.Damageable = true vehicle_terminal_combined.Repairable = true - vehicle_terminal_combined.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + vehicle_terminal_combined.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f vehicle_terminal_combined.RepairIfDestroyed = true vehicle_terminal_combined.Subtract.Damage1 = 8 @@ -7423,7 +7423,7 @@ object GlobalDefinitions { vanu_air_vehicle_term.MaxHealth = 500 vanu_air_vehicle_term.Damageable = true vanu_air_vehicle_term.Repairable = true - vanu_air_vehicle_term.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + vanu_air_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f vanu_air_vehicle_term.RepairIfDestroyed = true vanu_air_vehicle_term.Subtract.Damage1 = 8 @@ -7436,7 +7436,7 @@ object GlobalDefinitions { vanu_vehicle_term.MaxHealth = 500 vanu_vehicle_term.Damageable = true vanu_vehicle_term.Repairable = true - vanu_vehicle_term.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + vanu_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f vanu_vehicle_term.RepairIfDestroyed = true vanu_vehicle_term.Subtract.Damage1 = 8 @@ -7449,7 +7449,7 @@ object GlobalDefinitions { bfr_terminal.MaxHealth = 500 bfr_terminal.Damageable = true bfr_terminal.Repairable = true - bfr_terminal.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + bfr_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f bfr_terminal.RepairIfDestroyed = true bfr_terminal.Subtract.Damage1 = 8 @@ -7460,17 +7460,18 @@ object GlobalDefinitions { respawn_tube.Damageable = true respawn_tube.DamageableByFriendlyFire = false respawn_tube.Repairable = true - respawn_tube.autoRepair = AutoRepairStats(1, 10000, 2400, 1) + respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 respawn_tube.RepairIfDestroyed = true respawn_tube.Subtract.Damage1 = 8 respawn_tube_sanctuary.Name = "respawn_tube" respawn_tube_sanctuary.Delay = 10 respawn_tube_sanctuary.SpecificPointFunc = SpawnPoint.Default + respawn_tube_sanctuary.MaxHealth = 1000 respawn_tube_sanctuary.Damageable = false //true? respawn_tube_sanctuary.DamageableByFriendlyFire = false respawn_tube_sanctuary.Repairable = true - respawn_tube_sanctuary.autoRepair = AutoRepairStats(1, 10000, 2400, 1) + respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 respawn_tube_tower.Name = "respawn_tube_tower" respawn_tube_tower.Delay = 10 @@ -7479,7 +7480,7 @@ object GlobalDefinitions { respawn_tube_tower.Damageable = true respawn_tube_tower.DamageableByFriendlyFire = false respawn_tube_tower.Repairable = true - respawn_tube_tower.autoRepair = AutoRepairStats(1, 10000, 2400, 1) + respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 respawn_tube_tower.RepairIfDestroyed = true respawn_tube_tower.Subtract.Damage1 = 8 @@ -7497,7 +7498,7 @@ object GlobalDefinitions { medical_terminal.MaxHealth = 500 medical_terminal.Damageable = true medical_terminal.Repairable = true - medical_terminal.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + medical_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f medical_terminal.RepairIfDestroyed = true adv_med_terminal.Name = "adv_med_terminal" @@ -7509,7 +7510,7 @@ object GlobalDefinitions { adv_med_terminal.MaxHealth = 750 adv_med_terminal.Damageable = true adv_med_terminal.Repairable = true - adv_med_terminal.autoRepair = AutoRepairStats(1, 5000, 2400, 0.5f) + adv_med_terminal.autoRepair = AutoRepairStats(1.57894f, 5000, 2400, 0.5f) //orig. 1, 5000, 2400, 0.5f adv_med_terminal.RepairIfDestroyed = true crystals_health_a.Name = "crystals_health_a" @@ -7537,7 +7538,7 @@ object GlobalDefinitions { portable_med_terminal.MaxHealth = 500 portable_med_terminal.Damageable = false //TODO actually true portable_med_terminal.Repairable = false - portable_med_terminal.autoRepair = AutoRepairStats(1, 5000, 3500, 0.5f) + portable_med_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f pad_landing_frame.Name = "pad_landing_frame" pad_landing_frame.Interval = 1000 @@ -7651,7 +7652,7 @@ object GlobalDefinitions { manned_turret.Damageable = true manned_turret.DamageDisablesAt = 0 manned_turret.Repairable = true - manned_turret.autoRepair = AutoRepairStats(1, 10000, 1600, 0.5f) + manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.5f) //orig. 1, 10000, 1600, 0.5f manned_turret.RepairIfDestroyed = true manned_turret.Weapons += 1 -> new mutable.HashMap() manned_turret.Weapons(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan @@ -7675,7 +7676,7 @@ object GlobalDefinitions { vanu_sentry_turret.Damageable = true vanu_sentry_turret.DamageDisablesAt = 0 vanu_sentry_turret.Repairable = true - vanu_sentry_turret.autoRepair = AutoRepairStats(3, 10000, 1000, 0.5f) + vanu_sentry_turret.autoRepair = AutoRepairStats(3.27272f, 10000, 1000, 0.5f) //orig. 3, 10000, 1000, 0.5f vanu_sentry_turret.RepairIfDestroyed = true vanu_sentry_turret.Weapons += 1 -> new mutable.HashMap() vanu_sentry_turret.Weapons(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon @@ -7757,7 +7758,7 @@ object GlobalDefinitions { generator.Damageable = true generator.DamageableByFriendlyFire = false generator.Repairable = true - generator.autoRepair = AutoRepairStats(1, 5000, 875, 1) + generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1) //orig. 1, 5000, 875, 1 generator.RepairDistance = 13.5f generator.RepairIfDestroyed = true generator.Subtract.Damage1 = 9 diff --git a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala index 64b925d6..30d32252 100644 --- a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala +++ b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala @@ -9,6 +9,7 @@ import net.psforever.actors.zone.BuildingActor import net.psforever.objects.{Default, NtuContainer, NtuStorageBehavior} import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.structures.{Amenity, AutoRepairStats, Building} +import net.psforever.util.Config import scala.concurrent.duration._ @@ -38,6 +39,14 @@ trait AmenityAutoRepair private var autoRepairStartFunc: ()=>Unit = startAutoRepairIfStopped /** the timer for requests for auto-repair-actionable resource deposits (NTU) */ private var autoRepairTimer: Cancellable = Default.Cancellable + /** indicate the current state of the task assignment; + * `None` means no auto-repair operations; + * `Some(0L)` means previous auto-repair task completed; + * `Some(time)` means that an auto-repair task is or was queued to occur at `time` */ + private var autoRepairQueueTask: Option[Long] = None + /** repair can only occur in integer increments, so any non-integer portion of incremental repairs accumulates; + * once above a whole number, that number is extracted and applied to the base repair value */ + private var autoRepairOverflow: Float = 0f def AutoRepairObject: Amenity @@ -56,7 +65,7 @@ trait AmenityAutoRepair * Stop the auto-repair timer. */ def StopNtuBehavior(sender : ActorRef) : Unit = { - autoRepairTimer.cancel() + stopAutoRepair() } //nothing special @@ -73,7 +82,24 @@ trait AmenityAutoRepair val obj = AutoRepairObject obj.Definition.autoRepair match { case Some(repair : AutoRepairStats) if obj.Health < obj.Definition.MaxHealth => - PerformRepairs(obj, repair.amount) + autoRepairTimer.cancel() + val modifiedRepairAmount = repair.amount * Config.app.game.amenityAutorepairRate + val wholeRepairAmount = modifiedRepairAmount.toInt + val overflowRepairAmount = modifiedRepairAmount - wholeRepairAmount + val finalRepairAmount = if (autoRepairOverflow + overflowRepairAmount < 1) { + autoRepairOverflow += overflowRepairAmount + wholeRepairAmount + } else { + val totalOverflow = autoRepairOverflow + overflowRepairAmount + val wholeOverflow = totalOverflow.toInt + autoRepairOverflow = totalOverflow - wholeOverflow + wholeRepairAmount + wholeOverflow + } + PerformRepairs(obj, finalRepairAmount) + val currentTime = System.currentTimeMillis() + val taskTime = currentTime - autoRepairQueueTask.getOrElse(currentTime) + autoRepairQueueTask = Some(0L) + trySetupAutoRepairSubsequent(taskTime) case _ => StopNtuBehavior(sender) } @@ -101,7 +127,7 @@ trait AmenityAutoRepair * Set a function that will attempt auto-repair operations under specific trigger-able conditions (damage). */ private def startAutoRepairFunctionality(): Unit = { - retimeAutoRepair() + trySetupAutoRepairInitial() autoRepairStartFunc = startAutoRepairIfStopped } @@ -112,17 +138,39 @@ trait AmenityAutoRepair * @see `stopAutoRepair` */ private def stopAutoRepairFunctionality(): Unit = { - autoRepairTimer.cancel() + stopAutoRepair() autoRepairStartFunc = ()=>{} } /** * Attempt to start auto-repair operation - * only if no operation is currently being processed. + * only if no operation is currently being processed + * or if the current process has stalled. */ private def startAutoRepairIfStopped(): Unit = { - if(autoRepairTimer.isCancelled) { - retimeAutoRepair() + if(autoRepairQueueTask.isEmpty || stallDetection(stallTime = 15000L)) { + trySetupAutoRepairInitial() + } + } + + /** + * Detect if the auto-repair system is in a stalled state where orders are not being dispatched when they should. + * Not running or not being expected to be running does not count as being stalled. + * @param stallTime for how long we need to be stalled (ms) + * @return `true`, if stalled; + * `false`, otherwise + */ + private def stallDetection(stallTime: Long): Boolean = { + autoRepairQueueTask match { + case Some(0L) => + //the last auto-repair request was completed; did we start the next one? + autoRepairTimer.isCancelled + case Some(time) => + //waiting for too long on an active auto-repair request + time + stallTime > System.currentTimeMillis() + case None => + //we've not stalled; we're just not running + false } } @@ -148,9 +196,9 @@ trait AmenityAutoRepair * `false`, if it was already started, or did not start */ final def actuallyTryAutoRepair(): Boolean = { - val before = autoRepairTimer.isCancelled + val before = autoRepairQueueTask.isEmpty autoRepairStartFunc() - !(before || autoRepairTimer.isCancelled) + !(before || autoRepairQueueTask.isEmpty) } /** @@ -161,51 +209,77 @@ trait AmenityAutoRepair */ final def stopAutoRepair(): Unit = { autoRepairTimer.cancel() + autoRepairOverflow = 0 + autoRepairQueueTask = None } /** * As long as setup information regarding the auto-repair process can be discovered in the amenity's definition - * and the amenity actually requires to be performed, + * and the amenity actually requires repairs to be performed, * perform the setup for the auto-repair operation. + * This is the initial delay before the first repair attempt. */ - private def retimeAutoRepair(): Unit = { + private def trySetupAutoRepairInitial(): Unit = { val obj = AutoRepairObject obj.Definition.autoRepair match { - case Some(AutoRepairStats(_, start, interval, drain)) if obj.Health < obj.Definition.MaxHealth => - retimeAutoRepair(start, interval, drain) - case _ => ; + case Some(AutoRepairStats(_, start, _, drain)) if obj.Health < obj.Definition.MaxHealth => + setupAutoRepair(start, drain) + case _ => + stopAutoRepair() } } /** - * As long as setup information regarding the auto-repair process can be provided, + * As long as setup information regarding the auto-repair process can be discovered in the amenity's definition + * and the amenity actually requires repairs to be performed, * perform the setup for the auto-repair operation. - * @see `BuildingActor.Ntu` - * @see `NtuCommand.Request` - * @see `scheduleWithFixedDelay` - * @param initialDelay the delay before the first message - * @param delay the delay between subsequent messages, after the first - * @param drain the amount of NTU being levied as a cost for auto-repair operation - * (the responding entity determines how to satisfy the cost) + * This is the delay before every subsequent repair attempt. + * @param delayOffset an adjustment to the normal delay applied to the subsequent operation (ms); + * ideally, some number that's inclusive to 0 and the interval */ - private def retimeAutoRepair(initialDelay: Long, delay: Long, drain: Float): Unit = { + private def trySetupAutoRepairSubsequent(delayOffset: Long): Unit = { + if (autoRepairQueueTask.contains(0L)) { + val obj = AutoRepairObject + obj.Definition.autoRepair match { + case Some(AutoRepairStats(_, _, interval, drain)) if obj.Health < obj.Definition.MaxHealth => + setupAutoRepair( + math.min(interval, math.max(0L, interval - delayOffset)), + drain + ) + case _ => + stopAutoRepair() + } + } + } + + /** + * As long as setup information regarding the auto-repair process can be provided, + * perform the setup for the auto-repair operation. + * @see `BuildingActor.Ntu` + * @see `NtuCommand.Request` + * @see `scheduleOnce` + * @param delay the delay before the message is sent (ms) + * @param drain the amount of NTU being levied as a cost for auto-repair operation + * (the responding entity determines how to satisfy this cost) + */ + private def setupAutoRepair(delay: Long, drain: Float): Unit = { import scala.concurrent.ExecutionContext.Implicits.global autoRepairTimer.cancel() + autoRepairQueueTask = Some(System.currentTimeMillis() + delay) + val modifiedDrain = drain * Config.app.game.amenityAutorepairDrainRate autoRepairTimer = if(AutoRepairObject.Owner == Building.NoBuilding) { //without an owner, auto-repair freely - context.system.scheduler.scheduleWithFixedDelay( - initialDelay milliseconds, + context.system.scheduler.scheduleOnce( delay milliseconds, self, - NtuCommand.Grant(null, drain) + NtuCommand.Grant(null, modifiedDrain) ) } else { - //ask - context.system.scheduler.scheduleWithFixedDelay( - initialDelay milliseconds, + //ask politely + context.system.scheduler.scheduleOnce( delay milliseconds, AutoRepairObject.Owner.Actor, - BuildingActor.Ntu(NtuCommand.Request(drain, ntuGrantActorRef)) + BuildingActor.Ntu(NtuCommand.Request(modifiedDrain, ntuGrantActorRef)) ) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala index fc0f3d7c..684549a5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala @@ -24,7 +24,15 @@ class ResourceSilo extends Amenity with CommonNtuContainer { LowNtuWarningOn } - def CapacitorDisplay : Long = scala.math.ceil((NtuCapacitor / MaxNtuCapacitor) * 10).toInt + def CapacitorDisplay : Long = { + if(NtuCapacitor == 0) { + 0 + } else if(NtuCapacitor <= 0.1f * MaxNtuCapacitor) { + 1 + } else { + ((NtuCapacitor / MaxNtuCapacitor) * 10).toInt + } + } def Definition: ResourceSiloDefinition = GlobalDefinitions.resource_silo diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index 673829f8..0d03f5ab 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -12,6 +12,7 @@ import net.psforever.types.PlanetSideEmpire import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.util.Config import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -28,7 +29,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) def FactionObject: FactionAffinity = resourceSilo private[this] val log = org.log4s.getLogger - var panelAnimationFunc: Float => Unit = PanelAnimation + var panelAnimationFunc: (ActorRef, Float) => Unit = PanelAnimation def receive: Receive = { case "startup" => @@ -138,7 +139,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) */ def StopNtuBehavior(sender: ActorRef): Unit = { panelAnimationFunc = PanelAnimation - panelAnimationFunc(0) + panelAnimationFunc(sender, 0) } /** @@ -150,7 +151,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) */ def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = { val originalAmount = resourceSilo.NtuCapacitor - UpdateChargeLevel(-min) + UpdateChargeLevel(-(min * Config.app.game.amenityAutorepairDrainRate)) sender ! Ntu.Grant(resourceSilo, originalAmount - resourceSilo.NtuCapacitor) } @@ -159,9 +160,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) */ def HandleNtuGrant(sender: ActorRef, src: NtuContainer, amount: Float): Unit = { if (amount != 0) { - val originalAmount = resourceSilo.NtuCapacitor - UpdateChargeLevel(amount) - panelAnimationFunc(resourceSilo.NtuCapacitor - originalAmount) + panelAnimationFunc(sender, amount) panelAnimationFunc = SkipPanelAnimation } } @@ -174,16 +173,28 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) * @param trigger if positive, activate the animation; * if negative or zero, disable the animation */ - def PanelAnimation(trigger: Float): Unit = { + def PanelAnimation(source: ActorRef, trigger: Float): Unit = { val zone = resourceSilo.Zone zone.VehicleEvents ! VehicleServiceMessage( zone.id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, resourceSilo.GUID, 49, if (trigger > 0) 1 else 0) - ) // panel glow on & orb particles on + ) // panel glow & orb particles + // do not let the trigger charge go to waste, but also do not let the silo be filled + // attempting to return it to the source may sabotage an ongoing transfer process + val amount = math.min(resourceSilo.MaxNtuCapacitor - resourceSilo.NtuCapacitor, trigger) + UpdateChargeLevel(amount - amount*0.1f) } /** - * Do nothing this turn. + * Update the charge level and decide if the silo is full. + * Announce that full-ness to the NTU source. + * Although called "Skip", an animation that broadcasts the transfer process should be ongoing at the moment. */ - def SkipPanelAnimation(trigger: Float): Unit = {} + def SkipPanelAnimation(source: ActorRef, trigger: Float): Unit = { + UpdateChargeLevel(trigger) + // immediate termination of ntu requests + if (resourceSilo.NtuCapacitor == resourceSilo.MaxNtuCapacitor) { + source ! Ntu.Request(0, 0) + } + } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/AmenityDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/structures/AmenityDefinition.scala index bcf4b3ee..5141819f 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/AmenityDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/AmenityDefinition.scala @@ -7,7 +7,7 @@ import net.psforever.objects.vital._ import net.psforever.objects.vital.resistance.ResistanceProfileMutators import net.psforever.objects.vital.resolution.DamageResistanceModel -final case class AutoRepairStats(amount: Int, start: Long, repeat: Long, drain: Float) +final case class AutoRepairStats(amount: Float, start: Long, repeat: Long, drain: Float) abstract class AmenityDefinition(objectId: Int) extends ObjectDefinition(objectId) diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index 9ed800f4..b1c24b43 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -5,7 +5,7 @@ import java.util.concurrent.TimeUnit import akka.actor.ActorContext import net.psforever.actors.zone.BuildingActor -import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.{GlobalDefinitions, NtuContainer, Player} import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.hackable.Hackable @@ -92,6 +92,13 @@ class Building( } } + def NtuSource: Option[NtuContainer] = { + Amenities.find(_.isInstanceOf[NtuContainer]) match { + case Some(o: NtuContainer) => Some(o) + case _ => None + } + } + def NtuLevel: Int = { //if we have a silo, get the NTU level Amenities.find(_.Definition == GlobalDefinitions.resource_silo) match { diff --git a/src/main/scala/net/psforever/objects/serverobject/transfer/TransferBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/transfer/TransferBehavior.scala index e4e7125b..dc95c7ed 100644 --- a/src/main/scala/net/psforever/objects/serverobject/transfer/TransferBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/transfer/TransferBehavior.scala @@ -69,6 +69,7 @@ trait TransferBehavior { def TryStopChargingEvent(container : TransferContainer) : Unit = { transferEvent = TransferBehavior.Event.None transferTarget match { + case Some(_: net.psforever.objects.serverobject.structures.WarpGate) => ; case Some(obj) => obj.Actor ! TransferBehavior.Stopping() case _ => ; diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index 51090cf6..08b72739 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -124,6 +124,8 @@ case class SessionConfig( case class GameConfig( instantActionAms: Boolean, + amenityAutorepairRate: Float, + amenityAutorepairDrainRate: Float, bepRate: Double, cepRate: Double, newAvatar: NewAvatar diff --git a/src/test/scala/objects/AutoRepairIntegrationTest.scala b/src/test/scala/objects/AutoRepairIntegrationTest.scala deleted file mode 100644 index 0a64b8a1..00000000 --- a/src/test/scala/objects/AutoRepairIntegrationTest.scala +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) 2020 PSForever -package objects - -import akka.actor.Props -import akka.testkit.TestProbe -import base.FreedContextActorTest -import net.psforever.actors.zone.BuildingActor -import net.psforever.objects.avatar.Avatar -import net.psforever.objects.ballistics.{Projectile, SourceEntry} -import net.psforever.objects.guid.NumberPoolHub -import net.psforever.objects.guid.source.MaxNumberSource -import net.psforever.objects.serverobject.resourcesilo.{ResourceSilo, ResourceSiloControl} -import net.psforever.objects.serverobject.structures.{AutoRepairStats, Building, StructureType} -import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal, TerminalControl} -import net.psforever.objects.vital.Vitality -import net.psforever.objects.vital.base.DamageResolution -import net.psforever.objects.vital.damage.DamageProfile -import net.psforever.objects.vital.interaction.DamageInteraction -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.galaxy.GalaxyService -import net.psforever.services.{InterstellarClusterService, ServiceManager} -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} - -import scala.concurrent.duration._ - -class AutoRepairFacilityIntegrationTest extends FreedContextActorTest { - import akka.actor.typed.scaladsl.adapter._ - system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) - ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") - expectNoMessage(1000 milliseconds) - val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) - val avatarProbe = new TestProbe(system) - val zone = new Zone("test", new ZoneMap("test"), 0) { - override def SetupNumberPools() = {} - GUID(guid) - override def AvatarEvents = avatarProbe.ref - } - 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)) - player.Spawn() - val weapon = new Tool(GlobalDefinitions.suppressor) - val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition) - val silo = new ResourceSilo() - guid.register(player, number = 1) - guid.register(weapon, number = 2) - guid.register(weapon.AmmoSlot.Box, number = 3) - guid.register(terminal, number = 4) - guid.register(silo, number = 5) - guid.register(building, number = 6) - - building.Amenities = silo - building.Amenities = terminal - terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal") - silo.NtuCapacitor = 1000 - silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo") - silo.Actor ! "startup" - - val wep_fmode = weapon.FireMode - val wep_prof = wep_fmode.Add - val proj = weapon.Projectile - val proj_prof = proj.asInstanceOf[DamageProfile] - val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero) - val resolved = DamageInteraction( - SourceEntry(terminal), - ProjectileReason( - DamageResolution.Hit, - projectile, - terminal.DamageModel - ), - Vector3(1, 0, 0) - ) - val applyDamageTo = resolved.calculate() - - "AutoRepair" should { - "should activate on damage and trade NTU from the facility's resource silo for repairs" in { - assert(silo.NtuCapacitor == silo.MaxNtuCapacitor) - assert(terminal.Health == terminal.MaxHealth) - terminal.Actor ! Vitality.Damage(applyDamageTo) - - avatarProbe.receiveOne(max = 1000 milliseconds) //health update event - assert(terminal.Health < terminal.MaxHealth) - var i = 0 //safety counter - while(terminal.Health < terminal.MaxHealth && i < 100) { - i += 1 - avatarProbe.receiveOne(max = 1000 milliseconds) //health update event - } - assert(silo.NtuCapacitor < silo.MaxNtuCapacitor) - assert(terminal.Health == terminal.MaxHealth) - } - } -} - -class AutoRepairTowerIntegrationTest extends FreedContextActorTest { - import akka.actor.typed.scaladsl.adapter._ - system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id) - ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy") - expectNoMessage(1000 milliseconds) - val guid = new NumberPoolHub(new MaxNumberSource(max = 10)) - val avatarProbe = new TestProbe(system) - val zone = new Zone("test", new ZoneMap("test"), 0) { - override def SetupNumberPools() = {} - GUID(guid) - override def AvatarEvents = avatarProbe.ref - } - 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)) - player.Spawn() - val weapon = new Tool(GlobalDefinitions.suppressor) - val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition) - terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal") - guid.register(player, number = 1) - guid.register(weapon, number = 2) - guid.register(weapon.AmmoSlot.Box, number = 3) - guid.register(terminal, number = 4) - guid.register(building, number = 6) - - building.Amenities = terminal - building.Actor ! BuildingActor.SuppliedWithNtu() //artificial - building.Actor ! BuildingActor.PowerOn() //artificial - - val wep_fmode = weapon.FireMode - val wep_prof = wep_fmode.Add - val proj = weapon.Projectile - val proj_prof = proj.asInstanceOf[DamageProfile] - val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero) - val resolved = DamageInteraction( - SourceEntry(terminal), - ProjectileReason( - DamageResolution.Hit, - projectile, - terminal.DamageModel - ), - Vector3(1, 0, 0) - ) - val applyDamageTo = resolved.calculate() - - "AutoRepair" should { - "should activate on damage and trade NTU from the tower for repairs" in { - assert(terminal.Health == terminal.MaxHealth) - terminal.Actor ! Vitality.Damage(applyDamageTo) - - avatarProbe.receiveOne(max = 200 milliseconds) //health update event - assert(terminal.Health < terminal.MaxHealth) - var i = 0 //safety counter - while(terminal.Health < terminal.MaxHealth && i < 100) { - i += 1 - avatarProbe.receiveOne(max = 1000 milliseconds) //health update event - } - assert(terminal.Health == terminal.MaxHealth) - } - } -} - -object AutoRepairIntegrationTest { - val terminal_definition = new OrderTerminalDefinition(objId = 612) { - Name = "order_terminal" - MaxHealth = 500 - Damageable = true - Repairable = true - autoRepair = AutoRepairStats(200, 500, 500, 1) - RepairIfDestroyed = true - } -} diff --git a/src/test/scala/objects/ResourceSiloTest.scala b/src/test/scala/objects/ResourceSiloTest.scala index 36dab9b9..f0cd6c83 100644 --- a/src/test/scala/objects/ResourceSiloTest.scala +++ b/src/test/scala/objects/ResourceSiloTest.scala @@ -276,9 +276,9 @@ class ResourceSiloControlUpdate1Test extends ActorTest { val reply1 = zoneEvents.receiveOne(500 milliseconds) val reply2 = buildingEvents.receiveOne(500 milliseconds) assert(obj.NtuCapacitor == 305) - assert(obj.CapacitorDisplay == 4) + assert(obj.CapacitorDisplay == 3) assert(reply1 match { - case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(1), 45, 4)) => true + case AvatarServiceMessage("nowhere", AvatarAction.PlanetsideAttribute(PlanetSideGUID(1), 45, 3)) => true case _ => false }) assert(reply2.isInstanceOf[BuildingActor.MapUpdate]) @@ -321,7 +321,7 @@ class ResourceSiloControlUpdate2Test extends ActorTest { val reply1 = zoneEvents.receiveOne(1000 milliseconds) val reply2 = buildingEvents.receiveOne(1000 milliseconds) assert(obj.NtuCapacitor == 205) - assert(obj.CapacitorDisplay == 3) + assert(obj.CapacitorDisplay == 2) assert(reply1.isInstanceOf[AvatarServiceMessage]) assert(reply1.asInstanceOf[AvatarServiceMessage].forChannel == "nowhere") assert(reply1.asInstanceOf[AvatarServiceMessage].actionMessage.isInstanceOf[AvatarAction.PlanetsideAttribute]) @@ -344,7 +344,7 @@ class ResourceSiloControlUpdate2Test extends ActorTest { .asInstanceOf[AvatarServiceMessage] .actionMessage .asInstanceOf[AvatarAction.PlanetsideAttribute] - .attribute_value == 3 + .attribute_value == 2 ) assert(reply2.isInstanceOf[BuildingActor.MapUpdate]) @@ -400,18 +400,16 @@ class ResourceSiloControlNoUpdateTest extends ActorTest { obj.NtuCapacitor = 250 obj.LowNtuWarningOn = false assert(obj.NtuCapacitor == 250) - assert(obj.CapacitorDisplay == 3) + assert(obj.CapacitorDisplay == 2) assert(!obj.LowNtuWarningOn) - obj.Actor ! ResourceSilo.UpdateChargeLevel(50) + obj.Actor ! ResourceSilo.UpdateChargeLevel(49) expectNoMessage(500 milliseconds) zoneEvents.expectNoMessage(500 milliseconds) - buildingEvents.expectNoMessage(500 milliseconds) - assert( - obj.NtuCapacitor == 299 || obj.NtuCapacitor == 300 - ) // Just in case the capacitor level drops while waiting for the message check 299 & 300 - assert(obj.CapacitorDisplay == 3) + assert(obj.CapacitorDisplay == 2) + assert(obj.NtuCapacitor < 300) assert(!obj.LowNtuWarningOn) + buildingEvents.expectNoMessage(500 milliseconds) } } }