From 42c66bb4cac7221f8eab39c2f6a650707904ed63 Mon Sep 17 00:00:00 2001 From: Resaec Date: Sun, 12 Apr 2026 23:45:36 +0200 Subject: [PATCH 1/4] 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 + } } From 0ae8ab2418a1585d749d7c57f243fbfeb1a8ed19 Mon Sep 17 00:00:00 2001 From: Resaec Date: Mon, 13 Apr 2026 21:28:44 +0200 Subject: [PATCH 2/4] TimeOfDayMessage broadcast and fixup broadcast TimeOfDayMessage to all players in the zone fix input allows single minute digit times (0:0) fix set time(speed) before packet creation fix GetTimeOfDay return seconds, not millis fix SetTimeOfDay write correct integer type --- .../session/support/ChatOperations.scala | 32 +++++++++++++++---- .../net/psforever/objects/zones/Zone.scala | 6 ++-- 2 files changed, 28 insertions(+), 10 deletions(-) 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 814958bb0..ebb569428 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -15,6 +15,8 @@ import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.zones.{Zone, ZoneInfo} 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} @@ -1421,19 +1423,27 @@ class ChatOperations( } def commandSetTime(session: Session, contents: String): Unit = { - val TimePattern = """([01]?\d|2[0-3]):([0-5]\d)""".r + 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) + val zone = session.zone // update zone - session.zone.SetTimeOfDay(requestedTimeOfDay) + 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(msg) sendResponse(ChatMsg(messageType = UNK_227, contents = s"@CMT_SETTIME_OK^$hh~^$mm~")) case _ => sendResponse(ChatMsg(messageType = UNK_229, contents = "@CMT_SETTIME_usage")) @@ -1448,12 +1458,20 @@ class ChatOperations( var timeSpeed = m.matched.toFloat if (timeSpeed < -1000.0f) timeSpeed = -1000.0f if (timeSpeed > 1000.0f) timeSpeed = 1000.0f - val msg = TimeOfDayMessage(GetTimeOfDayValue(), timeSpeed) + val zone = session.zone // update zone - session.zone.SetTimeOfDaySpeed(timeSpeed) + 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(msg) sendResponse(ChatMsg(messageType = UNK_227, contents = s"@CMT_SETTIMESPEED_OK^$timeSpeed~")) case _ => sendResponse(ChatMsg(messageType = UNK_229, contents = "@CMT_SETTIMESPEED_usage")) diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 10c99dcea..8cad4927f 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -576,9 +576,9 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { // get time since origin val timeDiff = System.currentTimeMillis() - timeOfDayOrigin // get seconds - limit to 24h - val diffSeconds = timeDiff / 1000 % 86400 + val diffSeconds = (timeDiff / 1000) % 86400 - timeDiff.toFloat + diffSeconds.toFloat } def SetTimeOfDay(requestedTime: Float) = { @@ -590,7 +590,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { // substract requestedMillis from now to get new origin val newOrigin = now - requestMillis - timeOfDayOrigin = newOrigin.toInt + timeOfDayOrigin = newOrigin.toLong } def GetTimeOfDaySpeed(): Float = { From 7396b099989e2806f86dac3ff1fdbe8c49ab6b84 Mon Sep 17 00:00:00 2001 From: Resaec Date: Wed, 29 Apr 2026 23:28:49 +0200 Subject: [PATCH 3/4] fix SetTimeOfDaySpeed influencing "current" time fix GetTimeOfDay not considering time speed fix SetTimeOfDay not considering time speed --- .../net/psforever/objects/zones/Zone.scala | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 8cad4927f..b7686beb8 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -573,24 +573,25 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { } def GetTimeOfDay(): Float = { - // get time since origin - val timeDiff = System.currentTimeMillis() - timeOfDayOrigin - // get seconds - limit to 24h - val diffSeconds = (timeDiff / 1000) % 86400 + val now = System.currentTimeMillis() + val timeDiff = now - timeOfDayOrigin - diffSeconds.toFloat + // get seconds + val diffSeconds = timeDiff / 1000 + + // apply speed factor + val elapsedTime = diffSeconds * timeOfDaySpeed + + // wrap time to 24h + elapsedTime % 86400 } 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 + val requestedTimeDiff = requestedTime / timeOfDaySpeed - timeOfDayOrigin = newOrigin.toLong + timeOfDayOrigin = now - (requestedTimeDiff * 1000.0).toLong } def GetTimeOfDaySpeed(): Float = { @@ -598,7 +599,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { } def SetTimeOfDaySpeed(requestedSpeed: Float) = { + // store current time + val timeOfDay = GetTimeOfDay() + timeOfDaySpeed = requestedSpeed + + // restore old time with new speed considered + SetTimeOfDay(timeOfDay) } /** From 29b144a903fa29963fa64f9cd7a0f07006df5550 Mon Sep 17 00:00:00 2001 From: Resaec Date: Wed, 29 Apr 2026 23:29:43 +0200 Subject: [PATCH 4/4] fix /settime response not printing single digit minute values with leading zero --- .../net/psforever/actors/session/support/ChatOperations.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ebb569428..a08090b17 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -1444,7 +1444,7 @@ class ChatOperations( AvatarAction.SendResponse(Service.defaultPlayerGUID, msg) ) - sendResponse(ChatMsg(messageType = UNK_227, contents = s"@CMT_SETTIME_OK^$hh~^$mm~")) + sendResponse(ChatMsg(messageType = UNK_227, contents = f"@CMT_SETTIME_OK^$hh~^$mm%02d~")) case _ => sendResponse(ChatMsg(messageType = UNK_229, contents = "@CMT_SETTIME_usage")) }