Merge pull request #1357 from Resaec/TimeOfDayMessage

TimeOfDayMessage implementation
This commit is contained in:
Fate-JH 2026-05-02 11:56:11 -04:00 committed by GitHub
commit 2f9a6966ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 168 additions and 112 deletions

View file

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

View file

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

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

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

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