From c869cdc8a0fedfbc66846702f2535ab1f323b025 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 17 Aug 2023 23:55:17 -0400 Subject: [PATCH] early reivision for v010; recording ongoing shots fired and landed --- .../resources/db/migration/V010__Scoring2.sql | 335 ++++++++++++++++++ .../net/psforever/actors/net/LoginActor.scala | 13 +- .../actors/session/AvatarActor.scala | 66 +++- .../support/SessionAvatarHandlers.scala | 1 + .../WeaponAndProjectileOperations.scala | 135 ++++--- .../scala/net/psforever/objects/Vehicle.scala | 25 +- .../net/psforever/objects/Vehicles.scala | 46 +++ .../avatar/scoring/EquipmentStat.scala | 4 +- .../objects/avatar/scoring/ScoreCard.scala | 7 +- .../damage/DamageableVehicle.scala | 20 +- .../vehicles/control/VehicleControl.scala | 2 +- .../objects/zones/exp/ToDatabase.scala | 128 +++++++ .../net/psforever/persistence/Account.scala | 2 + .../psforever/persistence/Killactivity.scala | 16 + .../Machinedestroyedinstance.scala | 19 + .../persistence/Weaponstatsession.scala | 12 + 16 files changed, 736 insertions(+), 95 deletions(-) create mode 100644 server/src/main/resources/db/migration/V010__Scoring2.sql create mode 100644 src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala create mode 100644 src/main/scala/net/psforever/persistence/Killactivity.scala create mode 100644 src/main/scala/net/psforever/persistence/Machinedestroyedinstance.scala create mode 100644 src/main/scala/net/psforever/persistence/Weaponstatsession.scala diff --git a/server/src/main/resources/db/migration/V010__Scoring2.sql b/server/src/main/resources/db/migration/V010__Scoring2.sql new file mode 100644 index 000000000..a85aa1435 --- /dev/null +++ b/server/src/main/resources/db/migration/V010__Scoring2.sql @@ -0,0 +1,335 @@ +/* changes to objects from V008__Scoring.sql */ +ALTER TABLE killactivity +ADD COLUMN victim_mounted INT NO NULL DEFAULT 0; + +DROP PROCEDURE IF EXISTS proc_sessionnumber_initAndOrIncrease; + +CREATE OR REPLACE PROCEDURE proc_sessionnumber_initAndOrIncreasePerHour +(avatarId IN Int, number OUT Int, nextNumber OUT Int) +AS +$$ +DECLARE time TIMESTAMP; +BEGIN + SELECT MAX(session_id) INTO number + FROM sessionnumber + WHERE avatar_id = avatarId; + SELECT COALESCE(timestamp) INTO time + FROM sessionnumber + WHERE avatar_id = avatarId AND session_id = number; + IF (time IS null) THEN + number := 0; + nextNumber := 1; + INSERT INTO sessionnumber + VALUES (avatarId, nextNumber); + ELSIF (CURRENT_TIMESTAMP > DATE_TRUNC('hour', time) + interval '1' hour) THEN + nextNumber := number + 1; + INSERT INTO sessionnumber + VALUES (avatarId, nextNumber); + ELSE + nextNumber := number; + UPDATE sessionnumber + SET timestamp = CURRENT_TIMESTAMP + WHERE avatar_id = avatarId AND session_id = number; + END IF; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE PROCEDURE proc_sessionnumber_get +(avatarId IN Int, number OUT Int) +AS +$$ +DECLARE sessionId Int; +BEGIN + SELECT COALESCE(session_id) INTO sessionId + FROM account + WHERE avatar_logged_in = avatarId; + IF sessionId IS NULL THEN + number := 0; + ELSE + number := sessionId; + END IF; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE PROCEDURE proc_weaponstat_addEntryIfNone +(avatarId IN Int, weaponId IN Int) +AS +$$ +BEGIN + IF NOT EXISTS( + SELECT avatar_id, weapon_id + FROM weaponstat + WHERE avatar_id = avatarId AND weapon_id = weaponId + GROUP BY avatar_id, weapon_id + ) THEN + INSERT INTO weaponstat (avatar_id, weapon_id) + VALUES (avatarId, weaponId); + END IF; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE PROCEDURE proc_weaponstatsession_addEntryIfNoneWithSessionId +(avatarId IN Int, sessionId IN Int, weaponId IN Int) +AS +$$ +BEGIN + IF NOT EXISTS( + SELECT avatar_id, session_id, weapon_id + FROM weaponstatsession + WHERE avatar_id = avatarId AND weapon_id = weaponId AND session_id = sessionId + GROUP BY avatar_id, session_id, weapon_id + ) THEN + INSERT INTO weaponstatsession (avatar_id, session_id, weapon_id) + VALUES (avatarId, sessionId, weaponId); + END IF; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE PROCEDURE proc_weaponstatsession_addEntryIfNone +(avatarId IN Int, weaponId IN Int) +AS +$$ +DECLARE sessionId Int; +BEGIN + CALL proc_sessionnumber_get(avatarId, sessionId); + CALL proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, sessionId, weaponId); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION fn_account_newSession() +RETURNS TRIGGER +AS +$$ +DECLARE avatarId Int; +DECLARE sessionId Int; +DECLARE oldSessionId Int; +BEGIN + avatarId := NEW.avatar_logged_in; + CALL proc_sessionnumber_initAndOrIncreasePerHour(avatarId, oldSessionId, sessionId); + IF (sessionId > oldSessionId) THEN + BEGIN + UPDATE account + SET session_id = sessionId + WHERE id = OLD.id; + INSERT INTO kdasession (avatar_id, session_id) + VALUES (avatarId, sessionId); + END; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER psf_account_newSession +AFTER UPDATE +OF avatar_logged_in +ON account +FOR EACH ROW +WHEN (OLD.avatar_logged_in = 0 AND NEW.avatar_logged_in > 0) +EXECUTE FUNCTION fn_account_newSession(); + +CREATE OR REPLACE FUNCTION fn_killactivity_updateRelatedStats() +RETURNS TRIGGER +AS +$$ +DECLARE killerSessionId Int; +DECLARE victimSessionId Int; +DECLARE killerId Int; +DECLARE victimId Int; +DECLARE weaponId Int; +BEGIN + killerId := NEW.killer_id; + victimId := NEW.victim_id; + weaponId := NEW.weapon_id; + CALL proc_sessionnumber_get(killerId, killerSessionId); + CALL proc_sessionnumber_get(victimId, victimSessionId); + BEGIN + UPDATE kdasession + SET kills = kills + 1 + WHERE avatar_id = killerId AND session_id = killerSessionId; + END; + BEGIN + UPDATE kdasession + SET deaths = deaths + 1 + WHERE avatar_id = victimId AND session_id = victimSessionId; + END; + BEGIN + CALL proc_weaponstatsession_addEntryIfNoneWithSessionId(killerId, killerSessionId, weaponId); + UPDATE weaponstatsession + SET kills = kills + 1 + WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId; + END; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION fn_weaponstatsession_addEntryIfNone() +RETURNS TRIGGER +AS +$$ +DECLARE avatarId Int; +DECLARE weaponId Int; +BEGIN + avatarId := NEW.avatar_id; + weaponId := NEW.weapon_id; + CALL proc_weaponstatsession_addEntryIfNone(avatarId, weaponId); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION fn_kda_addEntryIfNone() +RETURNS TRIGGER +AS +$$ +DECLARE avatarId Int; +BEGIN + avatarId := NEW.avatar_id; + CALL proc_kda_addEntryIfNone(avatarId); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION fn_weaponstat_addEntryIfNone() +RETURNS TRIGGER +AS +$$ +DECLARE avatarId Int; +DECLARE weaponId Int; +BEGIN + avatarId := NEW.avatar_id; + weaponId := NEW.weapon_id; + CALL proc_weaponstat_addEntryIfNone(avatarId, weaponId); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION fn_kdasession_updateOnDelete() +RETURNS TRIGGER +AS +$$ +DECLARE avatarId Int; +DECLARE oldKills Int; +DECLARE oldDeaths Int; +DECLARE oldAssists Int; +BEGIN + avatarId := OLD.avatar_id; + oldKills := OLD.kills; + oldDeaths := OLD.deaths; + oldAssists := OLD.assists; + CALL proc_kda_addEntryIfNone(avatarId); + UPDATE kda + SET kills = kills + oldKills, + deaths = deaths + oldDeaths, + assists = assists + oldAssists + WHERE avatar_id = avatarId; + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION fn_weaponstatsession_updateOnDelete() +RETURNS TRIGGER +AS +$$ +DECLARE avatarId Int; +DECLARE weaponId Int; +DECLARE oldKills Int; +DECLARE oldAssists Int; +DECLARE oldFired Int; +DECLARE oldLanded Int; +BEGIN + avatarId := OLD.avatar_id; + weaponId := OLD.weapon_id; + oldKills := OLD.kills; + oldAssists := OLD.assists; + oldFired := OLD.shots_fired; + oldLanded := OLD.shots_landed; + CALL proc_weaponstat_addEntryIfNone(avatarId, weaponId); + UPDATE weaponstat + SET kills = kills + oldKills, + assists = assists + oldAssists, + shots_fired = shots_fired + oldFired, + shots_landed = shots_landed + oldLanded + WHERE avatar_id = avatarId AND weapon_id = weaponId; + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +/* new objects */ +CREATE OR REPLACE TABLE machinedestroyed ( + "index" SERIAL PRIMARY KEY NOT NULL, + "avatar_id" INT NOT NULL REFERENCES avatar (id), + "weapon_id" INT NOT NULL, + "machine_type" INT NOT NULL, + "machine_faction" SMALLINT NOT NULL, + "hacked_faction" SMALLINT NOT NULL, + "as_cargo" BOOLEAN NOT NULL, + "zone_num" SMALLINT NOT NULL, + "px" INT NOT NULL, + "py" INT NOT NULL, + "pz" INT NOT NULL, + "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +/* +expectations: +heal player with medapp: no intermediate_type, implement_type is the medapp (no, yes) +revive player with medapp: intermediate_type is 121, implement_type is the medapp (yes, yes) +repair max with bank: no intermediate_type, implement_type is the bank (no, yes) +repair foo with gluegun: intermediate_type is the foo object type, implement_type is the gluegun (yes, yes) +bail from vehicle: intermediate_type is the vehicle object type, no implement_type (yes, no) +cargo from vehicle: intermediate_type is the vehicle object type, implement_type is the cargo vehicle object type (yes, yes) +damage from vehicle (collision): no intermediate_type, implement_type is vehicle object type (no, yes) +damage from vehicle (explosion): intermediate_type is vehicle object type, implement_type is intermediate_type (yes, yes) +always ignore target_id = user_id +*/ +CREATE OR REPLACE TABLE supportactivity ( + "index" SERIAL PRIMARY KEY NOT NULL, + "user_id" INT NOT NULL REFERENCES avatar (id), -- player that provides the support + "target_id" INT NOT NULL REFERENCES avatar (id), -- benefactor of the support + "interaction_type" SMALLINT NOT NULL, -- description of support + "intermediate_type" INT DEFAULT 0, -- through what medium user_id supports target_id + "implement_type" INT DEFAULT 0, -- tool utilized by user_id to support target_id, potentially via interaction with intermediate_type + "bep" INT NOT NULL, + "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +/* +Before inserting a value into the weaponstatsession table to session id -1, +correct the session id to the most current session id, +and invalidate the attempted insertion at that -1 session id. +*/ +CREATE OR REPLACE FUNCTION fn_weaponstatsession_beforeInsert() +RETURNS TRIGGER +AS +$$ +DECLARE avatarId Int; +DECLARE weaponId Int; +DECLARE sessionId Int; +DECLARE oldAssists Int; +DECLARE oldFired Int; +DECLARE oldLanded Int; +BEGIN + avatarId := NEW.avatar_id; + weaponId := NEW.weapon_id; + oldAssists := NEW.assists; + oldFired := NEW.shots_fired; + oldLanded := NEW.shots_landed; + CALL proc_sessionnumber_get(avatarId, sessionId); + CALL proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, sessionId, weaponId); + BEGIN + UPDATE weaponstatsession + SET + assists = assists + oldAssists, + shots_fired = shots_fired + oldFired, + shots_landed = shots_landed + oldLanded + WHERE avatar_id = avatarId AND session_id = sessionId AND weapon_id = weaponId; + END; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER psf_weaponstatsession_beforeInsert +BEFORE INSERT +ON weaponstatsession +FOR EACH ROW +WHEN (NEW.session_id = -1) +EXECUTE FUNCTION fn_weaponstatsession_beforeInsert(); diff --git a/src/main/scala/net/psforever/actors/net/LoginActor.scala b/src/main/scala/net/psforever/actors/net/LoginActor.scala index 856399e65..3fe6cd46e 100644 --- a/src/main/scala/net/psforever/actors/net/LoginActor.scala +++ b/src/main/scala/net/psforever/actors/net/LoginActor.scala @@ -137,12 +137,10 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne bcryptedPassword } - def getAccountLogin(username: String, password: Option[String], token: Option[String]): Unit = { - - if (token.isDefined) { - accountLoginWithToken(token.getOrElse("")) - } else { - accountLogin(username, password.getOrElse("")) + def getAccountLogin(username: String, passwordOpt: Option[String], tokenOpt: Option[String]): Unit = { + tokenOpt match { + case Some(token) => accountLoginWithToken(token) + case None => accountLogin(username, passwordOpt.getOrElse("")) } } @@ -164,7 +162,8 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne accountOption <- accountsExact.headOption orElse accountsLower.headOption match { // account found - case Some(account) => Future.successful(Some(account)) + case Some(account) => + Future.successful(Some(account)) // create new account case None => diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 91306e806..1ba3637b6 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -9,6 +9,7 @@ import java.util.concurrent.atomic.AtomicInteger import net.psforever.actors.zone.ZoneActor import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill} import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.sourcing.VehicleSource import net.psforever.objects.vital.InGameHistory import net.psforever.objects.vehicles.MountedWeapons import org.joda.time.{LocalDateTime, Seconds} @@ -840,6 +841,15 @@ object AvatarActor { out.future } + def avatarNoLongerLoggedIn(accountId: Long): Unit = { + import ctx._ + ctx.run( + query[persistence.Account] + .filter(_.id == lift(accountId)) + .update(_.avatarLoggedIn -> lift(0L)) + ) + } + def toAvatar(avatar: persistence.Avatar): Avatar = { val bep = avatar.bep val convertedCosmetics = if (BattleRank.showCosmetics(bep)) { @@ -1044,13 +1054,13 @@ class AvatarActor( } yield true inits.onComplete { case Success(_) => - performAvatarLogin(avatarId, account.id, replyTo) + performAvatarLoginTest(avatarId, account.id, replyTo) case Failure(e) => log.error(e)("db failure") sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6)) } } else { - performAvatarLogin(avatarId, account.id, replyTo) + performAvatarLoginTest(avatarId, account.id, replyTo) } case Success(_) => //TODO this may not be an actual failure, but don't know what to do @@ -1069,6 +1079,11 @@ class AvatarActor( buffer.stash(other) Behaviors.same } + .receiveSignal { + case (_, PostStop) => + AvatarActor.avatarNoLongerLoggedIn(account.id) + Behaviors.same + } } def postCharacterSelectBehaviour(): Behavior[Command] = { @@ -1828,6 +1843,7 @@ class AvatarActor( } .receiveSignal { case (_, PostStop) => + AvatarActor.avatarNoLongerLoggedIn(account.get.id) AvatarActor.saveAvatarData(avatar) staminaRegenTimer.cancel() implantTimers.values.foreach(_.cancel()) @@ -1848,9 +1864,25 @@ class AvatarActor( Future.failed(ex).asInstanceOf[Future[Loadout]] } + def performAvatarLoginTest(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = { + import ctx._ + val blockLogInIfNot = for { + out <- ctx.run(query[persistence.Account].filter(_.id == lift(accountId))) + } yield out + blockLogInIfNot.onComplete { + case Success(account) + //TODO test against acceptable player factions + if account.exists { _.avatarLoggedIn == 0 } => + //accept + performAvatarLogin(avatarId, accountId, replyTo) + case _ => + //refuse + //TODO refuse + } + } + def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = { import ctx._ - val result = for { //log this login _ <- ctx.run( @@ -1862,7 +1894,10 @@ class AvatarActor( _ <- ctx.run( query[persistence.Account] .filter(_.id == lift(accountId)) - .update(_.lastFactionId -> lift(avatar.faction.id)) + .update( + _.lastFactionId -> lift(avatar.faction.id), + _.avatarLoggedIn -> lift(avatarId) + ) ) //retrieve avatar data loadouts <- initializeAllLoadouts() @@ -2995,8 +3030,26 @@ class AvatarActor( player.HistoryAndContributions() } zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, kill, exp) - case _: Assist => - () + val target = kill.info.targetAfter.asInstanceOf[PlayerSource] + val targetMounted = target.seatedIn.map { case (v: VehicleSource, seat) => + val definition = v.Definition + definition.ObjectId * 10 + Vehicles.SeatPermissionGroup(definition, seat).map { _.id }.getOrElse(0) + }.getOrElse(0) + zones.exp.ToDatabase.reportKillBy( + avatar.id.toLong, + target.CharId, + target.ExoSuit.id, + targetMounted, + kill.info.interaction.cause.attribution, + player.Zone.Number, + target.Position, + kill.experienceEarned + ) + case assist: Assist => + val avatarId = avatar.id.toLong + assist.weapons.foreach { + zones.exp.ToDatabase.reportAssistKills(avatarId, _, assists = 1) + } case _: Death => zone.AvatarEvents ! AvatarServiceMessage( player.Name, @@ -3019,6 +3072,7 @@ class AvatarActor( def updateToolDischarge(stats: EquipmentStat): Unit = { avatar.scorecard.rate(stats) + zones.exp.ToDatabase.reportToolDischarge(avatar.id.toLong, stats) } def createAvatar( diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 9a011532a..2af59a51e 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -542,6 +542,7 @@ class SessionAvatarHandlers( sessionData.terminals.CancelAllProximityUnits() sessionData.zoning AvatarActor.savePlayerLocation(player) + sessionData.shooting.reportOngoingShots(sessionData.shooting.reportOngoingShotsToDatabase) sessionData.zoning.spawn.shiftPosition = Some(player.Position) //respawn diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index c9bab2145..34c832e0d 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -2,6 +2,8 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} +import net.psforever.objects.zones.exp.ToDatabase + import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -43,7 +45,8 @@ private[support] class WeaponAndProjectileOperations( var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start private[support] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[support] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() - private var ongoingShotsFired: Int = 0 + private val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() + private val shotsLanded: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() private[support] var shotsWhileDead: Int = 0 private val projectiles: Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None) @@ -157,7 +160,7 @@ private[support] class WeaponAndProjectileOperations( fireStateStopPlayerMessages(item_guid) case Some(_) => fireStateStopMountedMessages(item_guid) - case _ => () + case _ => log.warn(s"ChangeFireState_Stop: can not find $item_guid") } sessionData.progressBarUpdate.cancel() @@ -275,8 +278,8 @@ private[support] class WeaponAndProjectileOperations( val LongRangeProjectileInfoMessage(guid, _, _) = pkt FindContainedWeapon match { case (Some(_: Vehicle), weapons) - if weapons.exists { _.GUID == guid } => ; //now what? - case _ => ; + if weapons.exists { _.GUID == guid } => () //now what? + case _ => () } } @@ -329,13 +332,11 @@ private[support] class WeaponAndProjectileOperations( _: Vector3, hitPos: Vector3 ) => - ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos) match { - case Some(resprojectile) => - avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0)) - sessionData.handleDealingDamage(target, resprojectile) - case None => ; + ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) + sessionData.handleDealingDamage(target, resprojectile) } - case _ => ; + case _ => () } case None => log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") @@ -368,26 +369,22 @@ private[support] class WeaponAndProjectileOperations( sessionData.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target) - ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match { - case Some(resprojectile) => - avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0)) - sessionData.handleDealingDamage(target, resprojectile) - case None => ; + ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) + sessionData.handleDealingDamage(target, resprojectile) } - case _ => ; + case _ => () } //other victims targets.foreach(elem => { sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match { - case Some(resprojectile) => - avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0)) - sessionData.handleDealingDamage(target, resprojectile) - case None => ; + ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) + sessionData.handleDealingDamage(target, resprojectile) } - case _ => ; + case _ => () } }) //... @@ -413,7 +410,7 @@ private[support] class WeaponAndProjectileOperations( continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) } } - case None => ; + case None => () } } @@ -422,13 +419,12 @@ private[support] class WeaponAndProjectileOperations( sessionData.validObject(victim_guid, decorator = "Lash") match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target) - ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos) match { - case Some(resprojectile) => - avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0)) + ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach { + resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) sessionData.handleDealingDamage(target, resprojectile) - case None => ; } - case _ => ; + case _ => () } } @@ -524,7 +520,7 @@ private[support] class WeaponAndProjectileOperations( obj match { case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret => turret.Actor ! FacilityTurret.WeaponDischarged() - case _ => ; + case _ => () } } else { log.warn( @@ -532,7 +528,7 @@ private[support] class WeaponAndProjectileOperations( ) } - case _ => ; + case _ => () } } @@ -552,7 +548,7 @@ private[support] class WeaponAndProjectileOperations( case Some(v: Vehicle) => //assert subsystem states v.SubsystemMessages().foreach { sendResponse } - case _ => ; + case _ => () } } if (enabledTools.nonEmpty) { @@ -577,8 +573,9 @@ private[support] class WeaponAndProjectileOperations( avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina) } avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) + tool.Discharge() prefire += weaponGUID - ongoingShotsFired = ongoingShotsFired + tool.Discharge() + addShotsFired(tool.Definition.ObjectId, tool.AmmoSlot.Chamber) } (o, Some(tool)) } @@ -635,7 +632,7 @@ private[support] class WeaponAndProjectileOperations( continent.GUID(weapon_guid) match { case Some(tool: Tool) => EmptyMagazine(weapon_guid, tool) - case _ => ; + case _ => () } } @@ -719,19 +716,17 @@ private[support] class WeaponAndProjectileOperations( */ def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { ModifyAmmunition(obj)(box, reloadValue) - obj.Find(box) match { - case Some(index) => - continent.VehicleEvents ! VehicleServiceMessage( - s"${obj.Actor}", - VehicleAction.InventoryState( - player.GUID, - box, - obj.GUID, - index, - box.Definition.Packet.DetailedConstructorData(box).get - ) + obj.Find(box).collect { index => + continent.VehicleEvents ! VehicleServiceMessage( + s"${obj.Actor}", + VehicleAction.InventoryState( + player.GUID, + box, + obj.GUID, + index, + box.Definition.Packet.DetailedConstructorData(box).get ) - case None => ; + ) } } @@ -849,7 +844,7 @@ private[support] class WeaponAndProjectileOperations( sessionData.normalItemDrop(player, continent)(previousBox) } AmmoBox.Split(previousBox) match { - case Nil | List(_) => ; //done (the former case is technically not possible) + case Nil | List(_) => () //done (the former case is technically not possible) case _ :: toUpdate => modifyFunc(previousBox, 0) //update to changed capacity value toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) }) @@ -1151,7 +1146,6 @@ private[support] class WeaponAndProjectileOperations( prefire -= itemGuid shooting += itemGuid shootingStart += itemGuid -> System.currentTimeMillis() - ongoingShotsFired = 0 } private def fireStateStartChargeMode(tool: Tool): Unit = { @@ -1220,11 +1214,10 @@ private[support] class WeaponAndProjectileOperations( used by ChangeFireStateMessage_Stop handling */ private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { - avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(tool.Definition.ObjectId, ongoingShotsFired, 0, 0)) tool.FireMode match { case _: ChargeFireModeDefinition => sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) - case _ => ; + case _ => () } if (tool.Magazine == 0) { FireCycleCleanup(tool) @@ -1365,6 +1358,49 @@ private[support] class WeaponAndProjectileOperations( ) } + private def addShotsFired(weaponId: Int, shots: Int): Unit = { + addShotsToMap(shotsFired, weaponId, shots) + } + + private def addShotsLanded(weaponId: Int, shots: Int): Unit = { + addShotsToMap(shotsLanded, weaponId, shots) + } + + private def addShotsToMap(map: mutable.HashMap[Int, Int], weaponId: Int, shots: Int): Unit = { + map.put( + weaponId, + map.get(weaponId) match { + case Some(previousShots) => previousShots + shots + case None => shots + } + ) + } + + private[support] def reportOngoingShots(reportFunc: (Long, Int, Int, Int) => Unit): Unit = { + reportOngoingShots(player.CharId, reportFunc) + } + + private[support] def reportOngoingShots(avatarId: Long, reportFunc: (Long, Int, Int, Int) => Unit): Unit = { + //only shots that have been reported as fired count + //if somehow shots had reported as landed but never reported as fired, they are ignored + //these are just raw counts; there's only numeric connection between the entries of fired and of landed + shotsFired.foreach { case (weaponId, fired) => + val landed = math.min(shotsLanded.getOrElse(weaponId, 0), fired) + reportFunc(avatarId, weaponId, fired, landed) + } + shotsFired.clear() + shotsLanded.clear() + } + + //noinspection ScalaUnusedSymbol + private[support] def reportOngoingShotsToAvatar(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = { + avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(weaponId, fired, landed, 0, 0)) + } + + private[support] def reportOngoingShotsToDatabase(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = { + ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0)) + } + override protected[session] def stop(): Unit = { if (player != null && player.HasGUID) { (prefire ++ shooting).foreach { guid => @@ -1373,6 +1409,7 @@ private[support] class WeaponAndProjectileOperations( fireStateStopMountedMessages(guid) } projectiles.indices.foreach { projectiles.update(_, None) } + reportOngoingShots(reportOngoingShotsToDatabase) } } } diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index b8e4eb039..df62c7596 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -267,30 +267,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) } def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = { - if (seatNumber == 0) { //valid in almost all cases - Some(AccessPermissionGroup.Driver) - } else { - Seat(seatNumber) match { - case Some(_) => - Definition.controlledWeapons().get(seatNumber) match { - case Some(_) => - Some(AccessPermissionGroup.Gunner) - case None => - Some(AccessPermissionGroup.Passenger) - } - case None => - CargoHold(seatNumber) match { - case Some(_) => - Some(AccessPermissionGroup.Passenger) //TODO confirm this - case None => - if (seatNumber >= trunk.Offset && seatNumber < trunk.Offset + trunk.TotalCapacity) { - Some(AccessPermissionGroup.Trunk) - } else { - None - } - } - } - } + Vehicles.SeatPermissionGroup(this.Definition, seatNumber) } def Utilities: Map[Int, Utility] = utilities diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index 5d2f3a61e..37d98cc60 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -2,6 +2,7 @@ package net.psforever.objects import net.psforever.objects.ce.TelepadLike +import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.resourcesilo.ResourceSilo @@ -439,4 +440,49 @@ object Vehicles { val turnAway = if (offset.x >= 0) -90f else 90f (obj.Position + offset, (shuttleAngle + turnAway) % 360f) } + + /** + * Based on a mounting index, for a certain mount, to what mounting group does this seat belong? + * @param vehicle the vehicle + * @param seatNumber specific seat index + * @return the seat group designation + */ + def SeatPermissionGroup(vehicle: Vehicle, seatNumber: Int): Option[AccessPermissionGroup.Value] = { + SeatPermissionGroup(vehicle.Definition, seatNumber) + } + + /** + * Based on a mounting index, for a certain mount, to what mounting group does this seat belong? + * @param definition global vehicle specification + * @param seatNumber specific seat index + * @return the seat group designation + */ + def SeatPermissionGroup(definition: VehicleDefinition, seatNumber: Int): Option[AccessPermissionGroup.Value] = { + if (seatNumber == 0) { //valid in almost all cases + Some(AccessPermissionGroup.Driver) + } else { + definition.Seats + .get(seatNumber) + .map { _ => + definition.controlledWeapons() + .get(seatNumber) + .map { _ => AccessPermissionGroup.Gunner } + .getOrElse { AccessPermissionGroup.Passenger } + } + .orElse { + definition.Cargo + .get(seatNumber) + .map { _ => AccessPermissionGroup.Passenger } + .orElse { + val offset = definition.TrunkOffset + val size = definition.TrunkSize + if (seatNumber >= offset && seatNumber < offset + size.Width * size.Height) { + Some(AccessPermissionGroup.Trunk) + } else { + None + } + } + } + } + } } diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/EquipmentStat.scala b/src/main/scala/net/psforever/objects/avatar/scoring/EquipmentStat.scala index 15c5c07db..1c136ba47 100644 --- a/src/main/scala/net/psforever/objects/avatar/scoring/EquipmentStat.scala +++ b/src/main/scala/net/psforever/objects/avatar/scoring/EquipmentStat.scala @@ -1,8 +1,8 @@ // Copyright (c) 2023 PSForever package net.psforever.objects.avatar.scoring -final case class EquipmentStat(objectId: Int, shotsFired: Int, shotsLanded: Int, kills: Int) +final case class EquipmentStat(objectId: Int, shotsFired: Int, shotsLanded: Int, kills: Int, assists: Int) object EquipmentStat { - def apply(objectId: Int): EquipmentStat = EquipmentStat(objectId, 0, 1, 0) + def apply(objectId: Int): EquipmentStat = EquipmentStat(objectId, 0, 1, 0, 0) } diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala index 23b8f7720..456362a07 100644 --- a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala +++ b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala @@ -30,7 +30,7 @@ class ScoreCard() { curr = ScoreCard.updateEquipmentStat(curr, e) case k: Kill => curr = curr.copy(kills = k +: curr.kills) - curr = ScoreCard.updateEquipmentStat(curr, EquipmentStat(k.info.interaction.cause.attribution, 0, 0, 1)) + curr = ScoreCard.updateEquipmentStat(curr, EquipmentStat(k.info.interaction.cause.attribution, 0, 0, 1, 0)) ScoreCard.updateStatisticsFor(killStatistics, k.info.interaction.cause.attribution, k.victim.Faction) case a: Assist => curr = curr.copy(assists = a +: curr.assists) @@ -51,14 +51,15 @@ class ScoreCard() { object ScoreCard { private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = { - updateEquipmentStat(curr, entry, entry.objectId, entry.kills) + updateEquipmentStat(curr, entry, entry.objectId, entry.kills, entry.assists) } private def updateEquipmentStat( curr: Life, entry: EquipmentStat, objectId: Int, - killCount: Int + killCount: Int, + assists: Int ): Life = { curr.equipmentStats.indexWhere { a => a.objectId == objectId } match { case -1 => diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index d1c2acf4b..2519ddc4e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -5,13 +5,15 @@ import akka.actor.{Actor, Cancellable} import net.psforever.objects.{Vehicle, Vehicles} import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.sourcing.VehicleSource import net.psforever.objects.vital.base.DamageResolution -import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.vital.interaction.{Adversarial, DamageResult} import net.psforever.objects.vital.resolution.ResolutionCalculations +import net.psforever.objects.zones.Zone +import net.psforever.objects.zones.exp.ToDatabase +import net.psforever.packet.game.DamageWithPositionMessage import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.objects.zones.Zone -import net.psforever.packet.game.DamageWithPositionMessage import net.psforever.types.Vector3 import scala.concurrent.duration._ @@ -213,6 +215,18 @@ trait DamageableVehicle VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, obj.Definition.shieldUiAttribute, 0) ) } + //database entry + cause.adversarial.collect { + case Adversarial(attacker, victim: VehicleSource, implement) => + ToDatabase.reportMachineDestruction( + attacker.CharId, + victim, + DamageableObject.HackedBy, + DamageableObject.MountedIn.nonEmpty, + implement, + obj.Zone.Number + ) + } //clean up target.Actor ! Vehicle.Deconstruct(Some(1 minute)) DamageableWeaponTurret.DestructionAwareness(obj, cause) diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index 210cc41c6..52e8146e8 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -298,7 +298,7 @@ class VehicleControl(vehicle: Vehicle) obj.PassengerInSeat(user) match { case Some(seatNumber) => val vsrc = VehicleSource(vehicle) - user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vehicle, vsrc), vehicle.Zone.Number)) + user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number)) //if the driver mount, change ownership if that is permissible for this vehicle if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) { //whatever vehicle was previously owned diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala new file mode 100644 index 000000000..9bd90e813 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala @@ -0,0 +1,128 @@ +// Copyright (c) 2022 PSForever +package net.psforever.objects.zones.exp + +import scala.concurrent.ExecutionContext.Implicits.global + +import net.psforever.objects.avatar.scoring.EquipmentStat +import net.psforever.objects.serverobject.hackable.Hackable.HackInfo +import net.psforever.objects.sourcing.VehicleSource +import net.psforever.persistence +import net.psforever.types.Vector3 +import net.psforever.util.Database.ctx +import net.psforever.util.Database.ctx._ + +import scala.util.Success + +object ToDatabase { + def reportKillBy( + killerId: Long, + victimId: Long, + victimExoSuitId: Int, + victimMounted: Int, + weaponId: Int, + zoneId: Int, + position: Vector3, + exp: Long + ): Unit = { + ctx.run(query[persistence.Killactivity] + .insert( + _.victimId -> lift(victimId), + _.killerId -> lift(killerId), + _.victimExosuit -> lift(victimExoSuitId), + _.victimMounted -> lift(victimMounted), + _.weaponId -> lift(weaponId), + _.zoneId -> lift(zoneId), + _.px -> lift((position.x * 1000).toInt), + _.py -> lift((position.y * 1000).toInt), + _.pz -> lift((position.z * 1000).toInt), + _.exp -> lift(exp) + ) + ) + } + + def reportToolDischarge(avatarId: Long, stats: EquipmentStat): Unit = { + val result = for { + res <- ctx.run( + query[persistence.Weaponstatsession] + .filter(_.avatarId == lift(avatarId)) + .filter(_.weaponId == lift(stats.objectId)) + .update( + _.shotsFired -> lift(stats.shotsFired), + _.shotsLanded -> lift(stats.shotsLanded) + ) + ) + } yield res + result.onComplete { + case Success(rowCount) if rowCount.longValue > 0 => () + case _ => + ctx.run(query[persistence.Weaponstatsession] + .insert( + _.avatarId -> lift(avatarId), + _.weaponId -> lift(stats.objectId), + _.shotsFired -> lift(stats.shotsFired), + _.shotsLanded -> lift(stats.shotsLanded), + _.kills -> lift(0), + _.assists -> lift(0), + _.sessionId -> lift(-1L) + ) + ) + } + } + + def reportAssistKills(avatarId: Long, weaponId: Int, assists: Int): Unit = { + val result = for { + res <- ctx.run( + query[persistence.Weaponstatsession] + .filter(_.avatarId == lift(avatarId)) + .filter(_.weaponId == lift(weaponId)) + .update( + _.assists -> lift(assists) + ) + ) + } yield res + result.onComplete { + case Success(rowCount) if rowCount.longValue > 0 => () + case _ => + ctx.run(query[persistence.Weaponstatsession] + .insert( + _.avatarId -> lift(avatarId), + _.weaponId -> lift(weaponId), + _.assists -> lift(assists), + _.shotsFired -> lift(0), + _.shotsLanded -> lift(0), + _.kills -> lift(0), + _.sessionId -> lift(-1L) + ) + ) + } + } + + def reportMachineDestruction( + avatarId: Long, + machine: VehicleSource, + hackState: Option[HackInfo], + isCargo: Boolean, + weaponId: Int, + zoneNumber: Int + ): Unit = { + import net.psforever.util.Database.ctx + import net.psforever.util.Database.ctx._ + val normalFaction = machine.Faction.id + val hackedFaction = hackState.map { _.player.Faction }.getOrElse(normalFaction) + val machinePosition = machine.Position + ctx.run(query[persistence.Machinedestroyedinstance] + .insert( + _.avatarId -> lift(avatarId), + _.weaponId -> lift(weaponId), + _.machineType -> lift(machine.Definition.ObjectId), + _.machineFaction -> lift(normalFaction), + _.hackedFaction -> lift(hackedFaction), + _.asCargo -> lift(isCargo), + _.zoneNum -> lift(zoneNumber), + _.px -> lift((machinePosition.x * 1000).toInt), + _.py -> lift((machinePosition.y * 1000).toInt), + _.pz -> lift((machinePosition.z * 1000).toInt) + ) + ) + } +} diff --git a/src/main/scala/net/psforever/persistence/Account.scala b/src/main/scala/net/psforever/persistence/Account.scala index 97a2382c3..090c12191 100644 --- a/src/main/scala/net/psforever/persistence/Account.scala +++ b/src/main/scala/net/psforever/persistence/Account.scala @@ -12,6 +12,8 @@ case class Account( inactive: Boolean = false, gm: Boolean = false, lastFactionId: Int = 3, + avatarLoggedIn: Long, + sessionId: Long, token: Option[String], tokenCreated: Option[LocalDateTime] ) diff --git a/src/main/scala/net/psforever/persistence/Killactivity.scala b/src/main/scala/net/psforever/persistence/Killactivity.scala new file mode 100644 index 000000000..d1448c1f4 --- /dev/null +++ b/src/main/scala/net/psforever/persistence/Killactivity.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2022 PSForever +package net.psforever.persistence + +case class Killactivity( + index: Int, + killerId: Long, + victimId: Long, + victimExosuit: Int, + victimMounted: Int, //object type id * 10 + seat type + weaponId: Int, + zoneId: Int, + px: Int, //Position.x * 1000 + py: Int, //Position.y * 1000 + pz: Int, //Position.z * 1000 + exp: Long + ) diff --git a/src/main/scala/net/psforever/persistence/Machinedestroyedinstance.scala b/src/main/scala/net/psforever/persistence/Machinedestroyedinstance.scala new file mode 100644 index 000000000..b876a2b5b --- /dev/null +++ b/src/main/scala/net/psforever/persistence/Machinedestroyedinstance.scala @@ -0,0 +1,19 @@ +// Copyright (c) 2022 PSForever +package net.psforever.persistence + +import org.joda.time.LocalDateTime + +case class Machinedestroyedinstance( + index: Int, + avatarId: Long, + weaponId: Int, + machineType: Int, + machineFaction: Int, + hackedFaction: Int, + asCargo: Boolean, + zoneNum: Int, + px: Int, //Position.x * 1000 + py: Int, //Position.y * 1000 + pz: Int, //Position.z * 1000 + timestamp: LocalDateTime = LocalDateTime.now() + ) diff --git a/src/main/scala/net/psforever/persistence/Weaponstatsession.scala b/src/main/scala/net/psforever/persistence/Weaponstatsession.scala new file mode 100644 index 000000000..7aca2bf82 --- /dev/null +++ b/src/main/scala/net/psforever/persistence/Weaponstatsession.scala @@ -0,0 +1,12 @@ +// Copyright (c) 2022 PSForever +package net.psforever.persistence + +case class Weaponstatsession( + avatarId: Long, + weaponId: Int, + shotsFired: Int, + shotsLanded: Int, + kills: Int, + assists: Int, + sessionId: Long + )