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 + } }