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
This commit is contained in:
Resaec 2026-04-12 23:45:36 +02:00
parent 575f5fd7fc
commit 42c66bb4ca
6 changed files with 143 additions and 112 deletions

View file

@ -97,6 +97,12 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_CAPTUREBASE, _, contents) => case (CMT_CAPTUREBASE, _, contents) =>
ops.commandCaptureBase(session, message, 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, _, _) => case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) =>
ops.commandSendToRecipient(session, message, comms) ops.commandSendToRecipient(session, message, comms)

View file

@ -13,7 +13,8 @@ import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.LivePlayerList import net.psforever.objects.LivePlayerList
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.zones.{Zone, ZoneInfo} 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.chat.{DefaultChannel, OutfitChannel, SquadChannel}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse} 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 = { override protected[session] def stop(): Unit = {
silenceTimer.cancel() silenceTimer.cancel()
chatService ! ChatService.LeaveAllChannels(chatServiceAdapter) chatService ! ChatService.LeaveAllChannels(chatServiceAdapter)

View file

@ -306,7 +306,10 @@ class ZoningOperations(
continent.VehicleEvents ! Service.Join(continentId) continent.VehicleEvents ! Service.Join(continentId)
continent.VehicleEvents ! Service.Join(factionChannel) continent.VehicleEvents ! Service.Join(factionChannel)
if (sessionLogic.connectionState != 100) configZone(continent) if (sessionLogic.connectionState != 100) configZone(continent)
sendResponse(TimeOfDayMessage(1135214592))
// set time of day
sendResponse(TimeOfDayMessage(continent.GetTimeOfDay(), continent.GetTimeOfDaySpeed()))
//custom //custom
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
@ -890,35 +893,35 @@ class ZoningOperations(
def handleTrainingZoneMessage(pkt: TrainingZoneMessage): Unit = { def handleTrainingZoneMessage(pkt: TrainingZoneMessage): Unit = {
pkt.zone.guid match { pkt.zone.guid match {
case 11 | 12 | 13 => case 11 | 12 | 13 =>
player.avatar.deployables.UpdateMaxCounts(player.avatar.certifications) player.avatar.deployables.UpdateMaxCounts(player.avatar.certifications)
RequestSanctuaryZoneSpawn(player, player.Zone.Number) RequestSanctuaryZoneSpawn(player, player.Zone.Number)
// leveraging SetZone for now because the VR zones have no "proper" spawn points configured yet // leveraging SetZone for now because the VR zones have no "proper" spawn points configured yet
case 17 => case 17 =>
if (player.Zone.id != "tzshtr") { if (player.Zone.id != "tzshtr") {
context.self ! SessionActor.SetZone("tzshtr", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size))) context.self ! SessionActor.SetZone("tzshtr", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
} }
case 18 => case 18 =>
if (player.Zone.id != "tzshnc") { if (player.Zone.id != "tzshnc") {
context.self ! SessionActor.SetZone("tzshnc", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size))) context.self ! SessionActor.SetZone("tzshnc", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
} }
case 19 => case 19 =>
if (player.Zone.id != "tzshvs") { if (player.Zone.id != "tzshvs") {
context.self ! SessionActor.SetZone("tzshvs", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size))) context.self ! SessionActor.SetZone("tzshvs", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
} }
case 20 => case 20 =>
if (player.Zone.id != "tzdrtr") { if (player.Zone.id != "tzdrtr") {
context.self ! SessionActor.SetZone("tzdrtr", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size))) context.self ! SessionActor.SetZone("tzdrtr", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
} }
case 21 => case 21 =>
if (player.Zone.id != "tzdrnc") { if (player.Zone.id != "tzdrnc") {
context.self ! SessionActor.SetZone("tzdrnc", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size))) context.self ! SessionActor.SetZone("tzdrnc", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
} }
case 22 => case 22 =>
if (player.Zone.id != "tzdrvs") { if (player.Zone.id != "tzdrvs") {
context.self ! SessionActor.SetZone("tzdrvs", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size))) 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}?") log.warn(s"Received TrainingZoneMessage that requests unexpected zone number ${pkt.zone.guid}?")
} }
} }

View file

@ -206,6 +206,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
*/ */
var benefitRecipient: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL 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. * When the zone has completed initializing, this will be the future.
* @see `init(ActorContext)` * @see `init(ActorContext)`
@ -565,6 +572,35 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
HotSpotTimeFunction 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. * 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:<br> * These messages are sent in this fashion at the time of joining the server:<br>
@ -1542,6 +1578,8 @@ object Zone {
zone.localEvents = context.actorOf(Props(classOf[LocalService], zone), s"$id-local-events") 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.vehicleEvents = context.actorOf(Props(classOf[VehicleService], zone), s"$id-vehicle-events")
zone.timeOfDayOrigin = System.currentTimeMillis()
BuildLocalObjects(zone)(context, guid) BuildLocalObjects(zone)(context, guid)
BuildSupportObjects(zone) BuildSupportObjects(zone)
MakeBuildings(zone)(context, guid) MakeBuildings(zone)(context, guid)
@ -1871,11 +1909,11 @@ object Zone {
val allAffectedTargets = pssos.filter { target => testTargetsFromZone(source, target, radius) } val allAffectedTargets = pssos.filter { target => testTargetsFromZone(source, target, radius) }
//inform remaining targets that they have suffered damage //inform remaining targets that they have suffered damage
allAffectedTargets allAffectedTargets
.foreach { target => .foreach { target =>
if (zone.id.startsWith("tz") && source.Faction == target.Faction) { if (zone.id.startsWith("tz") && source.Faction == target.Faction) {
//do not perform friendly-fire in VR zones //do not perform friendly-fire in VR zones
} else { } else {
target.Actor ! Vitality.Damage(createInteraction(source, target).calculate()) target.Actor ! Vitality.Damage(createInteraction(source, target).calculate())
} }
} }
allAffectedTargets allAffectedTargets

View file

@ -22,16 +22,15 @@ import scodec.codecs._
* <br> * <br>
* 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. * 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. * 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 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.)<br> * (Setting the time to 4294967040 exactly will reduce the rendering system to gibberish.)<br>
* <br> * <br>
* The interval from 1178164736 (~03:18) to 1203765248 (03:18) is about a full twenty-four hours.
* That is a count of 25600512. * @param timeOfDay Auraxis time
* @param time Auraxis time * @param timeSpeed consistently 10.0f
* @param unk consistently 1092616192; does nothing?
*/ */
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 type Packet = TimeOfDayMessage
def opcode = GamePacketOpcode.TimeOfDayMessage def opcode = GamePacketOpcode.TimeOfDayMessage
def encode = TimeOfDayMessage.encode(this) 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] { object TimeOfDayMessage extends Marshallable[TimeOfDayMessage] {
implicit val codec: Codec[TimeOfDayMessage] = ( implicit val codec: Codec[TimeOfDayMessage] = (
("time" | uint32L) :: ("timeOfDay" | floatL) ::
("unk" | uint32L) ("timeSpeed" | floatL)
).as[TimeOfDayMessage] ).as[TimeOfDayMessage]
}
/* def GetTimeOfDayValue(hours: Int, minutes: Int): Float = {
Time Testing Conducted in VS Sanctuary ((hours * 3600 + minutes * 60) % 86400).toFloat
48 00 __ __ __ 00 00 20 41 }
--------------------------
+01 00 00 def GetTimeOfDayValue(timeSpeed: Float = 10.0f): Float = {
-------------------------- val now = System.currentTimeMillis()
48 00 1B 00 47 00 00 20 41 //09:06 val scaled = now * timeSpeed
48 00 1C 00 47 00 00 20 41 //09:07 val seconds = scaled / 1_000 // back to seconds
... val clamped = seconds % 86400 // clamp to 24h
48 00 59 00 47 00 00 20 41 //09:08 (+3D 00 00)
-------------------------- clamped
+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)
*/

View file

@ -11,18 +11,37 @@ class TimeOfDayMessageTest extends Specification {
"decode" in { "decode" in {
PacketCoding.decodePacket(string).require match { PacketCoding.decodePacket(string).require match {
case TimeOfDayMessage(time, unk) => case TimeOfDayMessage(timeOfDay, timeSpeed) =>
time mustEqual 1191182336 timeOfDay mustEqual 32768.0f
unk mustEqual 1092616192 timeSpeed mustEqual 10.0f
case _ => case _ =>
ko ko
} }
} }
"encode" in { "encode" in {
val msg = TimeOfDayMessage(1191182336) val msg = TimeOfDayMessage(32768.0f)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string 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
}
} }