From 42c66bb4cac7221f8eab39c2f6a650707904ed63 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 12 Apr 2026 23:45:36 +0200 Subject: [PATCH] TimeOfDayMessage implementation corrected packet decoding added helper functions added zone logic to store and access local time updated zoning logic to use local zone time added chat commands to change zone local time and time speed --- .../actors/session/csr/ChatLogic.scala | 6 + .../session/support/ChatOperations.scala | 43 ++++++- .../session/support/ZoningOperations.scala | 21 ++-- .../net/psforever/objects/zones/Zone.scala | 42 ++++++- .../packet/game/TimeOfDayMessage.scala | 116 +++--------------- .../scala/game/TimeOfDayMessageTest.scala | 27 +++- 6 files changed, 143 insertions(+), 112 deletions(-) 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 55e947a8a..aee616c03 100644 --- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -97,6 +97,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..814958bb0 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,8 @@ 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.chat.{DefaultChannel, OutfitChannel, SquadChannel} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse} @@ -1419,6 +1420,46 @@ 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 msg = TimeOfDayMessage(requestedTimeOfDay) + + // update zone + session.zone.SetTimeOfDay(requestedTimeOfDay) + + sendResponse(msg) + sendResponse(ChatMsg(messageType = UNK_227, contents = s"@CMT_SETTIME_OK^$hh~^$mm~")) + 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 msg = TimeOfDayMessage(GetTimeOfDayValue(), timeSpeed) + + // update zone + session.zone.SetTimeOfDaySpeed(timeSpeed) + + sendResponse(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 99edd6d02..4816efb33 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 @@ -890,35 +893,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 05a871fa2..10c99dcea 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,35 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { HotSpotTimeFunction } + def GetTimeOfDay(): Float = { + // get time since origin + val timeDiff = System.currentTimeMillis() - timeOfDayOrigin + // get seconds - limit to 24h + val diffSeconds = timeDiff / 1000 % 86400 + + timeDiff.toFloat + } + + def SetTimeOfDay(requestedTime: Float) = { + // get current time + val now = System.currentTimeMillis() + // get requested time in millis + val requestMillis = requestedTime * 1000 + + // substract requestedMillis from now to get new origin + val newOrigin = now - requestMillis + + timeOfDayOrigin = newOrigin.toInt + } + + def GetTimeOfDaySpeed(): Float = { + timeOfDaySpeed + } + + def SetTimeOfDaySpeed(requestedSpeed: Float) = { + timeOfDaySpeed = requestedSpeed + } + /** * 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 +1578,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) @@ -1871,11 +1909,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 + } }