diff --git a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala
index eee8cb565..4e5e39395 100644
--- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala
@@ -98,6 +98,12 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_CAPTUREBASE, _, contents) =>
ops.commandCaptureBase(session, message, contents)
+ case (CMT_SETTIME, _, contents) =>
+ ops.commandSetTime(session, contents)
+
+ case (CMT_SETTIMESPEED, _, contents) =>
+ ops.commandSetTimeSpeed(session, contents)
+
case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) =>
ops.commandSendToRecipient(session, message, comms)
diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala
index 3cc25a788..a08090b17 100644
--- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala
@@ -13,7 +13,10 @@ import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.LivePlayerList
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.zones.{Zone, ZoneInfo}
-import net.psforever.packet.game.SetChatFilterMessage
+import net.psforever.packet.game.TimeOfDayMessage.GetTimeOfDayValue
+import net.psforever.packet.game.{SetChatFilterMessage, TimeOfDayMessage}
+import net.psforever.services.Service
+import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse}
@@ -1419,6 +1422,62 @@ class ChatOperations(
)
}
+ def commandSetTime(session: Session, contents: String): Unit = {
+ val TimePattern = """([01]?\d|2[0-3]):([0-5]?\d)""".r
+
+ TimePattern.findFirstMatchIn(contents) match {
+ case Some(m) =>
+ val hh = m.group(1).toInt % 24
+ val mm = m.group(2).toInt % 60
+ val requestedTimeOfDay = GetTimeOfDayValue(hh, mm)
+ val zone = session.zone
+
+ // update zone
+ zone.SetTimeOfDay(requestedTimeOfDay)
+
+ // build update message
+ val msg = TimeOfDayMessage(zone.GetTimeOfDay(), zone.GetTimeOfDaySpeed())
+
+ // update players in zone
+ zone.AvatarEvents ! AvatarServiceMessage(
+ zone.id,
+ AvatarAction.SendResponse(Service.defaultPlayerGUID, msg)
+ )
+
+ sendResponse(ChatMsg(messageType = UNK_227, contents = f"@CMT_SETTIME_OK^$hh~^$mm%02d~"))
+ case _ =>
+ sendResponse(ChatMsg(messageType = UNK_229, contents = "@CMT_SETTIME_usage"))
+ }
+ }
+
+ def commandSetTimeSpeed(session: Session, contents: String): Unit = {
+ val FloatPattern = """-?\d+(\.\d{1,2})?""".r
+
+ FloatPattern.findAllMatchIn(contents).toList match {
+ case List(m) =>
+ var timeSpeed = m.matched.toFloat
+ if (timeSpeed < -1000.0f) timeSpeed = -1000.0f
+ if (timeSpeed > 1000.0f) timeSpeed = 1000.0f
+ val zone = session.zone
+
+ // update zone
+ zone.SetTimeOfDaySpeed(timeSpeed)
+
+ // build update message
+ val msg = TimeOfDayMessage(zone.GetTimeOfDay(), zone.GetTimeOfDaySpeed())
+
+ // update players in zone
+ zone.AvatarEvents ! AvatarServiceMessage(
+ zone.id,
+ AvatarAction.SendResponse(Service.defaultPlayerGUID, msg)
+ )
+
+ sendResponse(ChatMsg(messageType = UNK_227, contents = s"@CMT_SETTIMESPEED_OK^$timeSpeed~"))
+ case _ =>
+ sendResponse(ChatMsg(messageType = UNK_229, contents = "@CMT_SETTIMESPEED_usage"))
+ }
+ }
+
override protected[session] def stop(): Unit = {
silenceTimer.cancel()
chatService ! ChatService.LeaveAllChannels(chatServiceAdapter)
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index b7d6162b7..5edd615d9 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -306,7 +306,10 @@ class ZoningOperations(
continent.VehicleEvents ! Service.Join(continentId)
continent.VehicleEvents ! Service.Join(factionChannel)
if (sessionLogic.connectionState != 100) configZone(continent)
- sendResponse(TimeOfDayMessage(1135214592))
+
+ // set time of day
+ sendResponse(TimeOfDayMessage(continent.GetTimeOfDay(), continent.GetTimeOfDaySpeed()))
+
//custom
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
@@ -879,35 +882,35 @@ class ZoningOperations(
def handleTrainingZoneMessage(pkt: TrainingZoneMessage): Unit = {
pkt.zone.guid match {
- case 11 | 12 | 13 =>
+ case 11 | 12 | 13 =>
player.avatar.deployables.UpdateMaxCounts(player.avatar.certifications)
RequestSanctuaryZoneSpawn(player, player.Zone.Number)
// leveraging SetZone for now because the VR zones have no "proper" spawn points configured yet
- case 17 =>
+ case 17 =>
if (player.Zone.id != "tzshtr") {
context.self ! SessionActor.SetZone("tzshtr", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
}
- case 18 =>
+ case 18 =>
if (player.Zone.id != "tzshnc") {
context.self ! SessionActor.SetZone("tzshnc", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
}
- case 19 =>
+ case 19 =>
if (player.Zone.id != "tzshvs") {
context.self ! SessionActor.SetZone("tzshvs", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
}
- case 20 =>
+ case 20 =>
if (player.Zone.id != "tzdrtr") {
context.self ! SessionActor.SetZone("tzdrtr", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
}
- case 21 =>
+ case 21 =>
if (player.Zone.id != "tzdrnc") {
context.self ! SessionActor.SetZone("tzdrnc", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
}
- case 22 =>
+ case 22 =>
if (player.Zone.id != "tzdrvs") {
context.self ! SessionActor.SetZone("tzdrvs", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
}
- case _ =>
+ case _ =>
log.warn(s"Received TrainingZoneMessage that requests unexpected zone number ${pkt.zone.guid}?")
}
}
diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala
index 0b708b1ff..60cc751dd 100644
--- a/src/main/scala/net/psforever/objects/zones/Zone.scala
+++ b/src/main/scala/net/psforever/objects/zones/Zone.scala
@@ -206,6 +206,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
*/
var benefitRecipient: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
+ /**
+ * Holds the origin time of this zones
+ * @see `TimeOfDayMessage`
+ */
+ private var timeOfDayOrigin: Long = 0
+ private var timeOfDaySpeed: Float = 10.0f
+
/**
* When the zone has completed initializing, this will be the future.
* @see `init(ActorContext)`
@@ -565,6 +572,42 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
HotSpotTimeFunction
}
+ def GetTimeOfDay(): Float = {
+ val now = System.currentTimeMillis()
+ val timeDiff = now - timeOfDayOrigin
+
+ // get seconds
+ val diffSeconds = timeDiff / 1000
+
+ // apply speed factor
+ val elapsedTime = diffSeconds * timeOfDaySpeed
+
+ // wrap time to 24h
+ elapsedTime % 86400
+ }
+
+ def SetTimeOfDay(requestedTime: Float) = {
+ val now = System.currentTimeMillis()
+
+ val requestedTimeDiff = requestedTime / timeOfDaySpeed
+
+ timeOfDayOrigin = now - (requestedTimeDiff * 1000.0).toLong
+ }
+
+ def GetTimeOfDaySpeed(): Float = {
+ timeOfDaySpeed
+ }
+
+ def SetTimeOfDaySpeed(requestedSpeed: Float) = {
+ // store current time
+ val timeOfDay = GetTimeOfDay()
+
+ timeOfDaySpeed = requestedSpeed
+
+ // restore old time with new speed considered
+ SetTimeOfDay(timeOfDay)
+ }
+
/**
* Provide bulk correspondence on all map entities that can be composed into packet messages and reported to a client.
* These messages are sent in this fashion at the time of joining the server:
@@ -1542,6 +1585,8 @@ object Zone {
zone.localEvents = context.actorOf(Props(classOf[LocalService], zone), s"$id-local-events")
zone.vehicleEvents = context.actorOf(Props(classOf[VehicleService], zone), s"$id-vehicle-events")
+ zone.timeOfDayOrigin = System.currentTimeMillis()
+
BuildLocalObjects(zone)(context, guid)
BuildSupportObjects(zone)
MakeBuildings(zone)(context, guid)
@@ -1898,11 +1943,11 @@ object Zone {
val allAffectedTargets = pssos.filter { target => testTargetsFromZone(source, target, radius) }
//inform remaining targets that they have suffered damage
allAffectedTargets
- .foreach { target =>
+ .foreach { target =>
if (zone.id.startsWith("tz") && source.Faction == target.Faction) {
//do not perform friendly-fire in VR zones
} else {
- target.Actor ! Vitality.Damage(createInteraction(source, target).calculate())
+ target.Actor ! Vitality.Damage(createInteraction(source, target).calculate())
}
}
allAffectedTargets
diff --git a/src/main/scala/net/psforever/packet/game/TimeOfDayMessage.scala b/src/main/scala/net/psforever/packet/game/TimeOfDayMessage.scala
index 7397c6aac..6bf824c3f 100644
--- a/src/main/scala/net/psforever/packet/game/TimeOfDayMessage.scala
+++ b/src/main/scala/net/psforever/packet/game/TimeOfDayMessage.scala
@@ -22,16 +22,15 @@ import scodec.codecs._
*
* If no time is set, the client starts counting from 10:00 at an initial rate of about one Auraxis minute every four or five real seconds.
* Setting the current time to 1107296256 sets the current time to 00:00 with an indeterminate, but slow, rate.
- * Time is normally initialized somewhere within an interval between 1174405120 and 1207959296.
+ * Time is normally initialized somewhere within an interval between 2:16 (0x460000, 1174405120) and 36:24 (0x47FFFF00, 1207959296).
* Setting the current time extremely high (near the numerical maximum) can cause psychedelic rendering.
* (Setting the time to 4294967040 exactly will reduce the rendering system to gibberish.)
*
- * The interval from 1178164736 (~03:18) to 1203765248 (03:18) is about a full twenty-four hours.
- * That is a count of 25600512.
- * @param time Auraxis time
- * @param unk consistently 1092616192; does nothing?
+
+ * @param timeOfDay Auraxis time
+ * @param timeSpeed consistently 10.0f
*/
-final case class TimeOfDayMessage(time: Long, unk: Long = 1092616192L) extends PlanetSideGamePacket {
+final case class TimeOfDayMessage(timeOfDay: Float, timeSpeed: Float = 10.0f) extends PlanetSideGamePacket {
type Packet = TimeOfDayMessage
def opcode = GamePacketOpcode.TimeOfDayMessage
def encode = TimeOfDayMessage.encode(this)
@@ -39,95 +38,20 @@ final case class TimeOfDayMessage(time: Long, unk: Long = 1092616192L) extends P
object TimeOfDayMessage extends Marshallable[TimeOfDayMessage] {
implicit val codec: Codec[TimeOfDayMessage] = (
- ("time" | uint32L) ::
- ("unk" | uint32L)
+ ("timeOfDay" | floatL) ::
+ ("timeSpeed" | floatL)
).as[TimeOfDayMessage]
-}
-/*
-Time Testing Conducted in VS Sanctuary
-48 00 __ __ __ 00 00 20 41
---------------------------
- +01 00 00
---------------------------
-48 00 1B 00 47 00 00 20 41 //09:06
-48 00 1C 00 47 00 00 20 41 //09:07
-...
-48 00 59 00 47 00 00 20 41 //09:08 (+3D 00 00)
---------------------------
- +10 00 00
---------------------------
-48 00 00 00 47 00 00 20 41 //09:06 <--
-48 00 10 00 47 00 00 20 41 //09:06
-48 00 20 00 47 00 00 20 41 //09:07
-48 00 30 00 47 00 00 20 41 //09:07
-48 00 40 00 47 00 00 20 41 //09:07
-48 00 50 00 47 00 00 20 41 //09:07
-48 00 60 00 47 00 00 20 41 //09:08
-48 00 70 00 47 00 00 20 41 //09:08
-48 00 80 00 47 00 00 20 41 //09:08
-48 00 90 00 47 00 00 20 41 //09:08
-48 00 A0 00 47 00 00 20 41 //09:09
-48 00 B0 00 47 00 00 20 41 //09:09
-48 00 C0 00 47 00 00 20 41 //09:09
-48 00 D0 00 47 00 00 20 41 //09:09
-48 00 E0 00 47 00 00 20 41 //09:10
-48 00 F0 00 47 00 00 20 41 //09:10
-48 00 00 01 47 00 00 20 41 //09:10
---------------------------
- +00 01 00
---------------------------
-48 00 00 00 47 00 00 20 41 //09:06 <--
-48 00 00 01 47 00 00 20 41 //09:10
-48 00 00 02 47 00 00 20 41 //09:15
-48 00 00 03 47 00 00 20 41 //09:19
-48 00 00 04 47 00 00 20 41 //09:23
-48 00 00 05 47 00 00 20 41 //09:27
-48 00 00 06 47 00 00 20 41 //09:32
-48 00 00 07 47 00 00 20 41 //09:36
-48 00 00 08 47 00 00 20 41 //09:40
-48 00 00 09 47 00 00 20 41 //09:44
-48 00 00 0A 47 00 00 20 41 //09:49
-48 00 00 0B 47 00 00 20 41 //09:53
-48 00 00 0C 47 00 00 20 41 //09:57
-48 00 00 0D 47 00 00 20 41 //09:01
-48 00 00 0E 47 00 00 20 41 //10:06
-48 00 00 0F 47 00 00 20 41 //10:10
-48 00 00 10 47 00 00 20 41 //10:14
---------------------------
- +00 10 00
---------------------------
-48 00 00 00 46 00 00 20 41 //02:17 (-00:17)
-48 00 00 10 46 00 00 20 41 //02:34 (-00:17)
-48 00 00 20 46 00 00 20 41 //02:51 (-00:17)
-48 00 00 30 46 00 00 20 41 //03:08 (-00:17)
-48 00 00 40 46 00 00 20 41 //03:25 (-00:17)
-48 00 00 50 46 00 00 20 41 //03:42 (-00:17)
-48 00 00 60 46 00 00 20 41 //03:59 (-00:17)
-48 00 00 70 46 00 00 20 41 //04:16 (-00:17)
-48 00 00 80 46 00 00 20 41 //04:33 (-00:34)
-48 00 00 90 46 00 00 20 41 //05:07 (-00:34)
-48 00 00 A0 46 00 00 20 41 //05:41 (-00:35)
-48 00 00 B0 46 00 00 20 41 //06:16 (-00:34)
-48 00 00 C0 46 00 00 20 41 //06:50 (-00:34)
-48 00 00 D0 46 00 00 20 41 //07:24 (-00:34)
-48 00 00 E0 46 00 00 20 41 //07:58 (-00:34)
-48 00 00 F0 46 00 00 20 41 //08:32 (-00:34)
-48 00 00 00 47 00 00 20 41 //09:06 <--
-48 00 00 10 47 00 00 20 41 //10:14 (+01:08)
-48 00 00 20 47 00 00 20 41 //11:23 (+01:09)
-48 00 00 30 47 00 00 20 41 //12:31 (+01:08)
-48 00 00 40 47 00 00 20 41 //13:39 (+01:08)
-48 00 00 50 47 00 00 20 41 //14:47 (+01:08)
-48 00 00 60 47 00 00 20 41 //15:56 (+01:08)
-48 00 00 70 47 00 00 20 41 //17:04 (+01:08)
-48 00 00 80 47 00 00 20 41 //18:12 (+01:08)
-48 00 00 90 47 00 00 20 41 //20:29 (+02:16)
-48 00 00 A0 47 00 00 20 41 //22:45 (+02:16)
-48 00 00 B0 47 00 00 20 41 //01:02 (+02:17)
-48 00 00 C0 47 00 00 20 41 //03:18 (+02:16)
-48 00 00 D0 47 00 00 20 41 //05:35 (+02:17)
-48 00 00 E0 47 00 00 20 41 //07:51 (+02:16)
-48 00 00 F0 47 00 00 20 41 //10:08 (+02:17)
-48 00 00 00 48 00 00 20 41 //12:24 (+02:16)
- */
+ def GetTimeOfDayValue(hours: Int, minutes: Int): Float = {
+ ((hours * 3600 + minutes * 60) % 86400).toFloat
+ }
+
+ def GetTimeOfDayValue(timeSpeed: Float = 10.0f): Float = {
+ val now = System.currentTimeMillis()
+ val scaled = now * timeSpeed
+ val seconds = scaled / 1_000 // back to seconds
+ val clamped = seconds % 86400 // clamp to 24h
+
+ clamped
+ }
+}
diff --git a/src/test/scala/game/TimeOfDayMessageTest.scala b/src/test/scala/game/TimeOfDayMessageTest.scala
index c2a1c5bbe..557e22e4a 100644
--- a/src/test/scala/game/TimeOfDayMessageTest.scala
+++ b/src/test/scala/game/TimeOfDayMessageTest.scala
@@ -11,18 +11,37 @@ class TimeOfDayMessageTest extends Specification {
"decode" in {
PacketCoding.decodePacket(string).require match {
- case TimeOfDayMessage(time, unk) =>
- time mustEqual 1191182336
- unk mustEqual 1092616192
+ case TimeOfDayMessage(timeOfDay, timeSpeed) =>
+ timeOfDay mustEqual 32768.0f
+ timeSpeed mustEqual 10.0f
case _ =>
ko
}
}
"encode" in {
- val msg = TimeOfDayMessage(1191182336)
+ val msg = TimeOfDayMessage(32768.0f)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string
}
+
+ val string2 = hex"48 00 00 A0 47 00 00 20 41" //22:45
+
+ "decode2" in {
+ PacketCoding.decodePacket(string2).require match {
+ case TimeOfDayMessage(timeOfDay, timeSpeed) =>
+ timeOfDay mustEqual 81920.0f
+ timeSpeed mustEqual 10.0f
+ case _ =>
+ ko
+ }
+ }
+
+ "encode2" in {
+ val msg = TimeOfDayMessage(81920.0f)
+ val pkt = PacketCoding.encodePacket(msg).require.toByteVector
+
+ pkt mustEqual string2
+ }
}