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) =>
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)

View file

@ -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)

View file

@ -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}?")
}
}

View file

@ -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:<br>
@ -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

View file

@ -22,16 +22,15 @@ import scodec.codecs._
* <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.
* 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.)<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 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
}
}

View file

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