mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-07-16 08:55:18 +00:00
early reivision for v010; recording ongoing shots fired and landed
This commit is contained in:
parent
ac0892ab34
commit
c869cdc8a0
16 changed files with 736 additions and 95 deletions
335
server/src/main/resources/db/migration/V010__Scoring2.sql
Normal file
335
server/src/main/resources/db/migration/V010__Scoring2.sql
Normal file
|
|
@ -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();
|
||||||
|
|
@ -137,12 +137,10 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
||||||
bcryptedPassword
|
bcryptedPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
def getAccountLogin(username: String, password: Option[String], token: Option[String]): Unit = {
|
def getAccountLogin(username: String, passwordOpt: Option[String], tokenOpt: Option[String]): Unit = {
|
||||||
|
tokenOpt match {
|
||||||
if (token.isDefined) {
|
case Some(token) => accountLoginWithToken(token)
|
||||||
accountLoginWithToken(token.getOrElse(""))
|
case None => accountLogin(username, passwordOpt.getOrElse(""))
|
||||||
} else {
|
|
||||||
accountLogin(username, password.getOrElse(""))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,7 +162,8 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
||||||
accountOption <- accountsExact.headOption orElse accountsLower.headOption match {
|
accountOption <- accountsExact.headOption orElse accountsLower.headOption match {
|
||||||
|
|
||||||
// account found
|
// account found
|
||||||
case Some(account) => Future.successful(Some(account))
|
case Some(account) =>
|
||||||
|
Future.successful(Some(account))
|
||||||
|
|
||||||
// create new account
|
// create new account
|
||||||
case None =>
|
case None =>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||||
import net.psforever.actors.zone.ZoneActor
|
import net.psforever.actors.zone.ZoneActor
|
||||||
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill}
|
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill}
|
||||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||||
|
import net.psforever.objects.sourcing.VehicleSource
|
||||||
import net.psforever.objects.vital.InGameHistory
|
import net.psforever.objects.vital.InGameHistory
|
||||||
import net.psforever.objects.vehicles.MountedWeapons
|
import net.psforever.objects.vehicles.MountedWeapons
|
||||||
import org.joda.time.{LocalDateTime, Seconds}
|
import org.joda.time.{LocalDateTime, Seconds}
|
||||||
|
|
@ -840,6 +841,15 @@ object AvatarActor {
|
||||||
out.future
|
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 = {
|
def toAvatar(avatar: persistence.Avatar): Avatar = {
|
||||||
val bep = avatar.bep
|
val bep = avatar.bep
|
||||||
val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
|
val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
|
||||||
|
|
@ -1044,13 +1054,13 @@ class AvatarActor(
|
||||||
} yield true
|
} yield true
|
||||||
inits.onComplete {
|
inits.onComplete {
|
||||||
case Success(_) =>
|
case Success(_) =>
|
||||||
performAvatarLogin(avatarId, account.id, replyTo)
|
performAvatarLoginTest(avatarId, account.id, replyTo)
|
||||||
case Failure(e) =>
|
case Failure(e) =>
|
||||||
log.error(e)("db failure")
|
log.error(e)("db failure")
|
||||||
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6))
|
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
performAvatarLogin(avatarId, account.id, replyTo)
|
performAvatarLoginTest(avatarId, account.id, replyTo)
|
||||||
}
|
}
|
||||||
case Success(_) =>
|
case Success(_) =>
|
||||||
//TODO this may not be an actual failure, but don't know what to do
|
//TODO this may not be an actual failure, but don't know what to do
|
||||||
|
|
@ -1069,6 +1079,11 @@ class AvatarActor(
|
||||||
buffer.stash(other)
|
buffer.stash(other)
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
}
|
}
|
||||||
|
.receiveSignal {
|
||||||
|
case (_, PostStop) =>
|
||||||
|
AvatarActor.avatarNoLongerLoggedIn(account.id)
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def postCharacterSelectBehaviour(): Behavior[Command] = {
|
def postCharacterSelectBehaviour(): Behavior[Command] = {
|
||||||
|
|
@ -1828,6 +1843,7 @@ class AvatarActor(
|
||||||
}
|
}
|
||||||
.receiveSignal {
|
.receiveSignal {
|
||||||
case (_, PostStop) =>
|
case (_, PostStop) =>
|
||||||
|
AvatarActor.avatarNoLongerLoggedIn(account.get.id)
|
||||||
AvatarActor.saveAvatarData(avatar)
|
AvatarActor.saveAvatarData(avatar)
|
||||||
staminaRegenTimer.cancel()
|
staminaRegenTimer.cancel()
|
||||||
implantTimers.values.foreach(_.cancel())
|
implantTimers.values.foreach(_.cancel())
|
||||||
|
|
@ -1848,9 +1864,25 @@ class AvatarActor(
|
||||||
Future.failed(ex).asInstanceOf[Future[Loadout]]
|
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 = {
|
def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
|
||||||
import ctx._
|
import ctx._
|
||||||
|
|
||||||
val result = for {
|
val result = for {
|
||||||
//log this login
|
//log this login
|
||||||
_ <- ctx.run(
|
_ <- ctx.run(
|
||||||
|
|
@ -1862,7 +1894,10 @@ class AvatarActor(
|
||||||
_ <- ctx.run(
|
_ <- ctx.run(
|
||||||
query[persistence.Account]
|
query[persistence.Account]
|
||||||
.filter(_.id == lift(accountId))
|
.filter(_.id == lift(accountId))
|
||||||
.update(_.lastFactionId -> lift(avatar.faction.id))
|
.update(
|
||||||
|
_.lastFactionId -> lift(avatar.faction.id),
|
||||||
|
_.avatarLoggedIn -> lift(avatarId)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
//retrieve avatar data
|
//retrieve avatar data
|
||||||
loadouts <- initializeAllLoadouts()
|
loadouts <- initializeAllLoadouts()
|
||||||
|
|
@ -2995,8 +3030,26 @@ class AvatarActor(
|
||||||
player.HistoryAndContributions()
|
player.HistoryAndContributions()
|
||||||
}
|
}
|
||||||
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, kill, exp)
|
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 =>
|
case _: Death =>
|
||||||
zone.AvatarEvents ! AvatarServiceMessage(
|
zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
player.Name,
|
player.Name,
|
||||||
|
|
@ -3019,6 +3072,7 @@ class AvatarActor(
|
||||||
|
|
||||||
def updateToolDischarge(stats: EquipmentStat): Unit = {
|
def updateToolDischarge(stats: EquipmentStat): Unit = {
|
||||||
avatar.scorecard.rate(stats)
|
avatar.scorecard.rate(stats)
|
||||||
|
zones.exp.ToDatabase.reportToolDischarge(avatar.id.toLong, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
def createAvatar(
|
def createAvatar(
|
||||||
|
|
|
||||||
|
|
@ -542,6 +542,7 @@ class SessionAvatarHandlers(
|
||||||
sessionData.terminals.CancelAllProximityUnits()
|
sessionData.terminals.CancelAllProximityUnits()
|
||||||
sessionData.zoning
|
sessionData.zoning
|
||||||
AvatarActor.savePlayerLocation(player)
|
AvatarActor.savePlayerLocation(player)
|
||||||
|
sessionData.shooting.reportOngoingShots(sessionData.shooting.reportOngoingShotsToDatabase)
|
||||||
sessionData.zoning.spawn.shiftPosition = Some(player.Position)
|
sessionData.zoning.spawn.shiftPosition = Some(player.Position)
|
||||||
|
|
||||||
//respawn
|
//respawn
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
package net.psforever.actors.session.support
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
import akka.actor.{ActorContext, typed}
|
import akka.actor.{ActorContext, typed}
|
||||||
|
import net.psforever.objects.zones.exp.ToDatabase
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
import scala.concurrent.Future
|
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
|
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 shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
|
||||||
private[support] var shootingStop: 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[support] var shotsWhileDead: Int = 0
|
||||||
private val projectiles: Array[Option[Projectile]] =
|
private val projectiles: Array[Option[Projectile]] =
|
||||||
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
|
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
|
||||||
|
|
@ -157,7 +160,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
fireStateStopPlayerMessages(item_guid)
|
fireStateStopPlayerMessages(item_guid)
|
||||||
case Some(_) =>
|
case Some(_) =>
|
||||||
fireStateStopMountedMessages(item_guid)
|
fireStateStopMountedMessages(item_guid)
|
||||||
case _ => ()
|
case _ =>
|
||||||
log.warn(s"ChangeFireState_Stop: can not find $item_guid")
|
log.warn(s"ChangeFireState_Stop: can not find $item_guid")
|
||||||
}
|
}
|
||||||
sessionData.progressBarUpdate.cancel()
|
sessionData.progressBarUpdate.cancel()
|
||||||
|
|
@ -275,8 +278,8 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
val LongRangeProjectileInfoMessage(guid, _, _) = pkt
|
val LongRangeProjectileInfoMessage(guid, _, _) = pkt
|
||||||
FindContainedWeapon match {
|
FindContainedWeapon match {
|
||||||
case (Some(_: Vehicle), weapons)
|
case (Some(_: Vehicle), weapons)
|
||||||
if weapons.exists { _.GUID == guid } => ; //now what?
|
if weapons.exists { _.GUID == guid } => () //now what?
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,13 +332,11 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
_: Vector3,
|
_: Vector3,
|
||||||
hitPos: Vector3
|
hitPos: Vector3
|
||||||
) =>
|
) =>
|
||||||
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos) match {
|
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
|
||||||
case Some(resprojectile) =>
|
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
|
|
||||||
sessionData.handleDealingDamage(target, resprojectile)
|
sessionData.handleDealingDamage(target, resprojectile)
|
||||||
case None => ;
|
|
||||||
}
|
}
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
case None =>
|
case None =>
|
||||||
log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found")
|
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 {
|
sessionData.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
|
||||||
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
||||||
CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target)
|
CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target)
|
||||||
ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match {
|
ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
|
||||||
case Some(resprojectile) =>
|
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
|
|
||||||
sessionData.handleDealingDamage(target, resprojectile)
|
sessionData.handleDealingDamage(target, resprojectile)
|
||||||
case None => ;
|
|
||||||
}
|
}
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
//other victims
|
//other victims
|
||||||
targets.foreach(elem => {
|
targets.foreach(elem => {
|
||||||
sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
|
sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
|
||||||
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
||||||
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
|
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
|
||||||
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match {
|
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
|
||||||
case Some(resprojectile) =>
|
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
|
|
||||||
sessionData.handleDealingDamage(target, resprojectile)
|
sessionData.handleDealingDamage(target, resprojectile)
|
||||||
case None => ;
|
|
||||||
}
|
}
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//...
|
//...
|
||||||
|
|
@ -413,7 +410,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
continent.Projectile ! ZoneProjectile.Remove(projectile.GUID)
|
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 {
|
sessionData.validObject(victim_guid, decorator = "Lash") match {
|
||||||
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
||||||
CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target)
|
CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target)
|
||||||
ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos) match {
|
ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach {
|
||||||
case Some(resprojectile) =>
|
resprojectile =>
|
||||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
|
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||||
sessionData.handleDealingDamage(target, resprojectile)
|
sessionData.handleDealingDamage(target, resprojectile)
|
||||||
case None => ;
|
|
||||||
}
|
}
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -524,7 +520,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
obj match {
|
obj match {
|
||||||
case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
|
case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
|
||||||
turret.Actor ! FacilityTurret.WeaponDischarged()
|
turret.Actor ! FacilityTurret.WeaponDischarged()
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn(
|
log.warn(
|
||||||
|
|
@ -532,7 +528,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -552,7 +548,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
case Some(v: Vehicle) =>
|
case Some(v: Vehicle) =>
|
||||||
//assert subsystem states
|
//assert subsystem states
|
||||||
v.SubsystemMessages().foreach { sendResponse }
|
v.SubsystemMessages().foreach { sendResponse }
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (enabledTools.nonEmpty) {
|
if (enabledTools.nonEmpty) {
|
||||||
|
|
@ -577,8 +573,9 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina)
|
avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina)
|
||||||
}
|
}
|
||||||
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
|
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
|
||||||
|
tool.Discharge()
|
||||||
prefire += weaponGUID
|
prefire += weaponGUID
|
||||||
ongoingShotsFired = ongoingShotsFired + tool.Discharge()
|
addShotsFired(tool.Definition.ObjectId, tool.AmmoSlot.Chamber)
|
||||||
}
|
}
|
||||||
(o, Some(tool))
|
(o, Some(tool))
|
||||||
}
|
}
|
||||||
|
|
@ -635,7 +632,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
continent.GUID(weapon_guid) match {
|
continent.GUID(weapon_guid) match {
|
||||||
case Some(tool: Tool) =>
|
case Some(tool: Tool) =>
|
||||||
EmptyMagazine(weapon_guid, tool)
|
EmptyMagazine(weapon_guid, tool)
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -719,8 +716,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
*/
|
*/
|
||||||
def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
|
def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
|
||||||
ModifyAmmunition(obj)(box, reloadValue)
|
ModifyAmmunition(obj)(box, reloadValue)
|
||||||
obj.Find(box) match {
|
obj.Find(box).collect { index =>
|
||||||
case Some(index) =>
|
|
||||||
continent.VehicleEvents ! VehicleServiceMessage(
|
continent.VehicleEvents ! VehicleServiceMessage(
|
||||||
s"${obj.Actor}",
|
s"${obj.Actor}",
|
||||||
VehicleAction.InventoryState(
|
VehicleAction.InventoryState(
|
||||||
|
|
@ -731,7 +727,6 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
box.Definition.Packet.DetailedConstructorData(box).get
|
box.Definition.Packet.DetailedConstructorData(box).get
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
case None => ;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -849,7 +844,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
sessionData.normalItemDrop(player, continent)(previousBox)
|
sessionData.normalItemDrop(player, continent)(previousBox)
|
||||||
}
|
}
|
||||||
AmmoBox.Split(previousBox) match {
|
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 =>
|
case _ :: toUpdate =>
|
||||||
modifyFunc(previousBox, 0) //update to changed capacity value
|
modifyFunc(previousBox, 0) //update to changed capacity value
|
||||||
toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) })
|
toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) })
|
||||||
|
|
@ -1151,7 +1146,6 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
prefire -= itemGuid
|
prefire -= itemGuid
|
||||||
shooting += itemGuid
|
shooting += itemGuid
|
||||||
shootingStart += itemGuid -> System.currentTimeMillis()
|
shootingStart += itemGuid -> System.currentTimeMillis()
|
||||||
ongoingShotsFired = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def fireStateStartChargeMode(tool: Tool): Unit = {
|
private def fireStateStartChargeMode(tool: Tool): Unit = {
|
||||||
|
|
@ -1220,11 +1214,10 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
used by ChangeFireStateMessage_Stop handling
|
used by ChangeFireStateMessage_Stop handling
|
||||||
*/
|
*/
|
||||||
private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = {
|
private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = {
|
||||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(tool.Definition.ObjectId, ongoingShotsFired, 0, 0))
|
|
||||||
tool.FireMode match {
|
tool.FireMode match {
|
||||||
case _: ChargeFireModeDefinition =>
|
case _: ChargeFireModeDefinition =>
|
||||||
sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine))
|
sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine))
|
||||||
case _ => ;
|
case _ => ()
|
||||||
}
|
}
|
||||||
if (tool.Magazine == 0) {
|
if (tool.Magazine == 0) {
|
||||||
FireCycleCleanup(tool)
|
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 = {
|
override protected[session] def stop(): Unit = {
|
||||||
if (player != null && player.HasGUID) {
|
if (player != null && player.HasGUID) {
|
||||||
(prefire ++ shooting).foreach { guid =>
|
(prefire ++ shooting).foreach { guid =>
|
||||||
|
|
@ -1373,6 +1409,7 @@ private[support] class WeaponAndProjectileOperations(
|
||||||
fireStateStopMountedMessages(guid)
|
fireStateStopMountedMessages(guid)
|
||||||
}
|
}
|
||||||
projectiles.indices.foreach { projectiles.update(_, None) }
|
projectiles.indices.foreach { projectiles.update(_, None) }
|
||||||
|
reportOngoingShots(reportOngoingShotsToDatabase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -267,30 +267,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = {
|
def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = {
|
||||||
if (seatNumber == 0) { //valid in almost all cases
|
Vehicles.SeatPermissionGroup(this.Definition, seatNumber)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def Utilities: Map[Int, Utility] = utilities
|
def Utilities: Map[Int, Utility] = utilities
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
package net.psforever.objects
|
package net.psforever.objects
|
||||||
|
|
||||||
import net.psforever.objects.ce.TelepadLike
|
import net.psforever.objects.ce.TelepadLike
|
||||||
|
import net.psforever.objects.definition.VehicleDefinition
|
||||||
import net.psforever.objects.serverobject.CommonMessages
|
import net.psforever.objects.serverobject.CommonMessages
|
||||||
import net.psforever.objects.serverobject.deploy.Deployment
|
import net.psforever.objects.serverobject.deploy.Deployment
|
||||||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
||||||
|
|
@ -439,4 +440,49 @@ object Vehicles {
|
||||||
val turnAway = if (offset.x >= 0) -90f else 90f
|
val turnAway = if (offset.x >= 0) -90f else 90f
|
||||||
(obj.Position + offset, (shuttleAngle + turnAway) % 360f)
|
(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright (c) 2023 PSForever
|
// Copyright (c) 2023 PSForever
|
||||||
package net.psforever.objects.avatar.scoring
|
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 {
|
object EquipmentStat {
|
||||||
def apply(objectId: Int): EquipmentStat = EquipmentStat(objectId, 0, 1, 0)
|
def apply(objectId: Int): EquipmentStat = EquipmentStat(objectId, 0, 1, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class ScoreCard() {
|
||||||
curr = ScoreCard.updateEquipmentStat(curr, e)
|
curr = ScoreCard.updateEquipmentStat(curr, e)
|
||||||
case k: Kill =>
|
case k: Kill =>
|
||||||
curr = curr.copy(kills = k +: curr.kills)
|
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)
|
ScoreCard.updateStatisticsFor(killStatistics, k.info.interaction.cause.attribution, k.victim.Faction)
|
||||||
case a: Assist =>
|
case a: Assist =>
|
||||||
curr = curr.copy(assists = a +: curr.assists)
|
curr = curr.copy(assists = a +: curr.assists)
|
||||||
|
|
@ -51,14 +51,15 @@ class ScoreCard() {
|
||||||
|
|
||||||
object ScoreCard {
|
object ScoreCard {
|
||||||
private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = {
|
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(
|
private def updateEquipmentStat(
|
||||||
curr: Life,
|
curr: Life,
|
||||||
entry: EquipmentStat,
|
entry: EquipmentStat,
|
||||||
objectId: Int,
|
objectId: Int,
|
||||||
killCount: Int
|
killCount: Int,
|
||||||
|
assists: Int
|
||||||
): Life = {
|
): Life = {
|
||||||
curr.equipmentStats.indexWhere { a => a.objectId == objectId } match {
|
curr.equipmentStats.indexWhere { a => a.objectId == objectId } match {
|
||||||
case -1 =>
|
case -1 =>
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@ import akka.actor.{Actor, Cancellable}
|
||||||
import net.psforever.objects.{Vehicle, Vehicles}
|
import net.psforever.objects.{Vehicle, Vehicles}
|
||||||
import net.psforever.objects.equipment.JammableUnit
|
import net.psforever.objects.equipment.JammableUnit
|
||||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
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.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.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.Service
|
||||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
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 net.psforever.types.Vector3
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
@ -213,6 +215,18 @@ trait DamageableVehicle
|
||||||
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, obj.Definition.shieldUiAttribute, 0)
|
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
|
//clean up
|
||||||
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
|
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
|
||||||
DamageableWeaponTurret.DestructionAwareness(obj, cause)
|
DamageableWeaponTurret.DestructionAwareness(obj, cause)
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ class VehicleControl(vehicle: Vehicle)
|
||||||
obj.PassengerInSeat(user) match {
|
obj.PassengerInSeat(user) match {
|
||||||
case Some(seatNumber) =>
|
case Some(seatNumber) =>
|
||||||
val vsrc = VehicleSource(vehicle)
|
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 the driver mount, change ownership if that is permissible for this vehicle
|
||||||
if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) {
|
if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) {
|
||||||
//whatever vehicle was previously owned
|
//whatever vehicle was previously owned
|
||||||
|
|
|
||||||
128
src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
Normal file
128
src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
Normal file
|
|
@ -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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,8 @@ case class Account(
|
||||||
inactive: Boolean = false,
|
inactive: Boolean = false,
|
||||||
gm: Boolean = false,
|
gm: Boolean = false,
|
||||||
lastFactionId: Int = 3,
|
lastFactionId: Int = 3,
|
||||||
|
avatarLoggedIn: Long,
|
||||||
|
sessionId: Long,
|
||||||
token: Option[String],
|
token: Option[String],
|
||||||
tokenCreated: Option[LocalDateTime]
|
tokenCreated: Option[LocalDateTime]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
16
src/main/scala/net/psforever/persistence/Killactivity.scala
Normal file
16
src/main/scala/net/psforever/persistence/Killactivity.scala
Normal file
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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()
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue