From 7845508bd3ccf40bd15745f2d538628e77baa081 Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 13 Oct 2017 16:26:10 -0400 Subject: [PATCH] moved AvatarService and LocalService into their own package --- pslogin/src/main/scala/PsLogin.scala | 2 + .../src/main/scala/WorldSessionActor.scala | 13 ++++- .../main/scala/{ => services}/Service.scala | 5 ++ .../scala/services/avatar/AvatarAction.scala | 28 +++++++++ .../{ => services/avatar}/AvatarService.scala | 58 +------------------ .../avatar/AvatarServiceMessage.scala | 4 ++ .../avatar/AvatarServiceResponse.scala | 34 +++++++++++ .../scala/services/local/LocalAction.scala | 18 ++++++ .../{ => services/local}/LocalService.scala | 48 ++++----------- .../services/local/LocalServiceMessage.scala | 4 ++ .../services/local/LocalServiceResponse.scala | 21 +++++++ .../local/support}/DoorCloseActor.scala | 3 +- .../local/support}/HackClearActor.scala | 3 +- 13 files changed, 144 insertions(+), 97 deletions(-) rename pslogin/src/main/scala/{ => services}/Service.scala (85%) create mode 100644 pslogin/src/main/scala/services/avatar/AvatarAction.scala rename pslogin/src/main/scala/{ => services/avatar}/AvatarService.scala (64%) create mode 100644 pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala create mode 100644 pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala create mode 100644 pslogin/src/main/scala/services/local/LocalAction.scala rename pslogin/src/main/scala/{ => services/local}/LocalService.scala (53%) create mode 100644 pslogin/src/main/scala/services/local/LocalServiceMessage.scala create mode 100644 pslogin/src/main/scala/services/local/LocalServiceResponse.scala rename {common/src/main/scala/net/psforever/objects/zones => pslogin/src/main/scala/services/local/support}/DoorCloseActor.scala (98%) rename {common/src/main/scala/net/psforever/objects/zones => pslogin/src/main/scala/services/local/support}/HackClearActor.scala (98%) diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index 94551628..6e172cbe 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -18,6 +18,8 @@ import net.psforever.objects.serverobject.builders.{DoorObjectBuilder, IFFLockOb import org.slf4j import org.fusesource.jansi.Ansi._ import org.fusesource.jansi.Ansi.Color._ +import services.avatar._ +import services.local._ import scala.collection.JavaConverters._ import scala.concurrent.Await diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 2ed2191f..7f6634d5 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -2,9 +2,9 @@ import java.util.concurrent.atomic.AtomicInteger import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} -import net.psforever.packet.{PlanetSideGamePacket, _} +import net.psforever.packet._ import net.psforever.packet.control._ -import net.psforever.packet.game.{ObjectCreateDetailedMessage, _} +import net.psforever.packet.game._ import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC @@ -23,6 +23,9 @@ import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.packet.game.objectcreate._ import net.psforever.types._ +import services._ +import services.avatar._ +import services.local._ import scala.annotation.tailrec import scala.util.Success @@ -229,13 +232,15 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17))) case LocalServiceResponse.HackClear(target_guid, unk1, unk2) => - log.info(s"Clear hack of $target_guid") sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2))) case LocalServiceResponse.HackObject(target_guid, unk1, unk2) => if(player.GUID != guid) { sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2))) } + + case LocalServiceResponse.TriggerSound(sound, pos, unk, volume) => + sendResponse(PacketCoding.CreateGamePacket(0, TriggerSoundMessage(sound, pos, unk, volume))) } case Door.DoorMessage(tplayer, msg, order) => @@ -1631,8 +1636,10 @@ class WorldSessionActor extends Actor with MDCContextAware { * @see `HackMessage` */ //TODO add params here depending on which params in HackMessage are important + //TODO sound should be centered on IFFLock, not on player private def FinishHackingDoor(target : IFFLock, unk : Long)() : Unit = { target.Actor ! CommonMessages.Hack(player) + localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, TriggeredSound.HackDoor, player.Position, 30, 0.49803925f)) localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk)) } diff --git a/pslogin/src/main/scala/Service.scala b/pslogin/src/main/scala/services/Service.scala similarity index 85% rename from pslogin/src/main/scala/Service.scala rename to pslogin/src/main/scala/services/Service.scala index 37a9c87c..eed17c79 100644 --- a/pslogin/src/main/scala/Service.scala +++ b/pslogin/src/main/scala/services/Service.scala @@ -1,8 +1,13 @@ // Copyright (c) 2017 PSForever +package services + import akka.event.{ActorEventBus, SubchannelClassification} import akka.util.Subclassification +import net.psforever.packet.game.PlanetSideGUID object Service { + final val defaultPlayerGUID : PlanetSideGUID = PlanetSideGUID(0) + final case class Join(channel : String) final case class Leave() final case class LeaveAll() diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala new file mode 100644 index 00000000..451b3873 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package services.avatar + +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} +import net.psforever.packet.game.objectcreate.ConstructorData +import net.psforever.types.{ExoSuitType, Vector3} + +object AvatarAction { + trait Action + + final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action + //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action + final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action + final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action + final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action +// final case class LoadMap(msg : PlanetSideGUID) extends Action +// final case class unLoadMap(msg : PlanetSideGUID) extends Action + final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action + final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action + final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action + final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action + final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action +// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action +// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action +} diff --git a/pslogin/src/main/scala/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala similarity index 64% rename from pslogin/src/main/scala/AvatarService.scala rename to pslogin/src/main/scala/services/avatar/AvatarService.scala index 5905366f..5b656c23 100644 --- a/pslogin/src/main/scala/AvatarService.scala +++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala @@ -1,60 +1,8 @@ // Copyright (c) 2017 PSForever +package services.avatar + import akka.actor.Actor -import net.psforever.objects.equipment.Equipment -import net.psforever.packet.game.objectcreate.ConstructorData -import net.psforever.types.ExoSuitType -import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} -import net.psforever.types.Vector3 - -object AvatarAction { - trait Action - - final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action - //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action - final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action - final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action - final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action -// final case class LoadMap(msg : PlanetSideGUID) extends Action -// final case class unLoadMap(msg : PlanetSideGUID) extends Action - final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action - final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action - final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action - final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action - final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action -// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action -// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action -// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action -// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action -} - -object AvatarServiceResponse { - trait Response - - final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response - //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response - final case class EquipmentInHand(slot : Int, item : Equipment) extends Response - final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response - final case class LoadPlayer(pdata : ConstructorData) extends Response -// final case class unLoadMap() extends Response -// final case class LoadMap() extends Response - final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response - final case class ObjectHeld(slot : Int) extends Response - final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response - final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response - final case class Reload(mag : Int) extends Response -// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response -// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response -// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response -// final case class ChangeWeapon(facingYaw : Int) extends Response -} - -final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action) - -final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : AvatarServiceResponse.Response) extends GenericEventBusMsg - -/* - /Avatar/ - */ +import services.{GenericEventBus, Service} class AvatarService extends Actor { //import AvatarServiceResponse._ diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala new file mode 100644 index 00000000..e3e35cd3 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -0,0 +1,4 @@ +// Copyright (c) 2017 PSForever +package services.avatar + +final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action) diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala new file mode 100644 index 00000000..0bec7e74 --- /dev/null +++ b/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -0,0 +1,34 @@ +// Copyright (c) 2017 PSForever +package services.avatar + +import net.psforever.objects.equipment.Equipment +import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} +import net.psforever.packet.game.objectcreate.ConstructorData +import net.psforever.types.{ExoSuitType, Vector3} +import services.GenericEventBusMsg + +final case class AvatarServiceResponse(toChannel : String, + avatar_guid : PlanetSideGUID, + replyMessage : AvatarServiceResponse.Response + ) extends GenericEventBusMsg + +object AvatarServiceResponse { + trait Response + + final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response + //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response + final case class EquipmentInHand(slot : Int, item : Equipment) extends Response + final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response + final case class LoadPlayer(pdata : ConstructorData) extends Response +// final case class unLoadMap() extends Response +// final case class LoadMap() extends Response + final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response + final case class ObjectHeld(slot : Int) extends Response + final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response + final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response + final case class Reload(mag : Int) extends Response +// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response +// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response +// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response +// final case class ChangeWeapon(facingYaw : Int) extends Response +} \ No newline at end of file diff --git a/pslogin/src/main/scala/services/local/LocalAction.scala b/pslogin/src/main/scala/services/local/LocalAction.scala new file mode 100644 index 00000000..4003fd9b --- /dev/null +++ b/pslogin/src/main/scala/services/local/LocalAction.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2017 PSForever +package services.local + +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} +import net.psforever.types.Vector3 + +object LocalAction { + trait Action + + final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action + final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action + final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action + final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action + final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action +} diff --git a/pslogin/src/main/scala/LocalService.scala b/pslogin/src/main/scala/services/local/LocalService.scala similarity index 53% rename from pslogin/src/main/scala/LocalService.scala rename to pslogin/src/main/scala/services/local/LocalService.scala index 165016ef..56acf25d 100644 --- a/pslogin/src/main/scala/LocalService.scala +++ b/pslogin/src/main/scala/services/local/LocalService.scala @@ -1,35 +1,9 @@ // Copyright (c) 2017 PSForever +package services.local + import akka.actor.{Actor, Props} -import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.zones.{DoorCloseActor, HackClearActor, Zone} -import net.psforever.packet.game.PlanetSideGUID - -object LocalAction { - trait Action - - final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action - final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action - final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action - final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action -} - -object LocalServiceResponse { - trait Response - - final case class DoorOpens(door_guid : PlanetSideGUID) extends Response - final case class DoorCloses(door_guid : PlanetSideGUID) extends Response - final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response - final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response -} - -final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action) - -final case class LocalServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : LocalServiceResponse.Response) extends GenericEventBusMsg - -/* - /LocalEnvironment/ - */ +import services.local.support.{DoorCloseActor, HackClearActor} +import services.{GenericEventBus, Service} class LocalService extends Actor { //import LocalService._ @@ -72,7 +46,11 @@ class LocalService extends Actor { case LocalAction.HackTemporarily(player_guid, zone, target, unk1, unk2) => hackClearer ! HackClearActor.ObjectIsHacked(target, zone, unk1, unk2) LocalEvents.publish( - LocalServiceResponse(s"/$forChannel/Avatar", player_guid, LocalServiceResponse.HackObject(target.GUID, unk1, unk2)) + LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.HackObject(target.GUID, unk1, unk2)) + ) + case LocalAction.TriggerSound(player_guid, sound, pos, unk, volume) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.TriggerSound(sound, pos, unk, volume)) ) case _ => ; } @@ -80,20 +58,16 @@ class LocalService extends Actor { //response from DoorCloseActor case DoorCloseActor.CloseTheDoor(door_guid, zone_id) => LocalEvents.publish( - LocalServiceResponse(s"/$zone_id/LocalEnvironment", LocalService.defaultPlayerGUID, LocalServiceResponse.DoorCloses(door_guid)) + LocalServiceResponse(s"/$zone_id/LocalEnvironment", Service.defaultPlayerGUID, LocalServiceResponse.DoorCloses(door_guid)) ) //response from HackClearActor case HackClearActor.ClearTheHack(target_guid, zone_id, unk1, unk2) => LocalEvents.publish( - LocalServiceResponse(s"/$zone_id/LocalEnvironment", LocalService.defaultPlayerGUID, LocalServiceResponse.HackClear(target_guid, unk1, unk2)) + LocalServiceResponse(s"/$zone_id/LocalEnvironment", Service.defaultPlayerGUID, LocalServiceResponse.HackClear(target_guid, unk1, unk2)) ) case msg => log.info(s"Unhandled message $msg from $sender") } } - -object LocalService { - final val defaultPlayerGUID : PlanetSideGUID = PlanetSideGUID(0) -} diff --git a/pslogin/src/main/scala/services/local/LocalServiceMessage.scala b/pslogin/src/main/scala/services/local/LocalServiceMessage.scala new file mode 100644 index 00000000..fc6dd20a --- /dev/null +++ b/pslogin/src/main/scala/services/local/LocalServiceMessage.scala @@ -0,0 +1,4 @@ +// Copyright (c) 2017 PSForever +package services.local + +final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action) diff --git a/pslogin/src/main/scala/services/local/LocalServiceResponse.scala b/pslogin/src/main/scala/services/local/LocalServiceResponse.scala new file mode 100644 index 00000000..736732bc --- /dev/null +++ b/pslogin/src/main/scala/services/local/LocalServiceResponse.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2017 PSForever +package services.local + +import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} +import net.psforever.types.Vector3 +import services.GenericEventBusMsg + +final case class LocalServiceResponse(toChannel : String, + avatar_guid : PlanetSideGUID, + replyMessage : LocalServiceResponse.Response + ) extends GenericEventBusMsg + +object LocalServiceResponse { + trait Response + + final case class DoorOpens(door_guid : PlanetSideGUID) extends Response + final case class DoorCloses(door_guid : PlanetSideGUID) extends Response + final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response + final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response + final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response +} diff --git a/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala b/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala similarity index 98% rename from common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala rename to pslogin/src/main/scala/services/local/support/DoorCloseActor.scala index 4b2d9e9f..a2ca622c 100644 --- a/common/src/main/scala/net/psforever/objects/zones/DoorCloseActor.scala +++ b/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala @@ -1,8 +1,9 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.zones +package services.local.support import akka.actor.{Actor, Cancellable} import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID import scala.annotation.tailrec diff --git a/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala b/pslogin/src/main/scala/services/local/support/HackClearActor.scala similarity index 98% rename from common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala rename to pslogin/src/main/scala/services/local/support/HackClearActor.scala index 2ec3cb05..76a7e7f9 100644 --- a/common/src/main/scala/net/psforever/objects/zones/HackClearActor.scala +++ b/pslogin/src/main/scala/services/local/support/HackClearActor.scala @@ -1,8 +1,9 @@ // Copyright (c) 2017 PSForever -package net.psforever.objects.zones +package services.local.support import akka.actor.{Actor, Cancellable} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID import scala.annotation.tailrec