command experience calculations upon facility capture or resecure

This commit is contained in:
Fate-JH 2023-10-06 23:58:59 -04:00
parent 8bdb215d58
commit 77066f962e
48 changed files with 1544 additions and 1020 deletions

View file

@ -17,9 +17,10 @@ CREATE TABLE IF NOT EXISTS "sessionnumber" (
CREATE TABLE IF NOT EXISTS "killactivity" ( CREATE TABLE IF NOT EXISTS "killactivity" (
"index" SERIAL PRIMARY KEY NOT NULL, "index" SERIAL PRIMARY KEY NOT NULL,
"victim_id" INT NOT NULL REFERENCES avatar (id),
"killer_id" INT NOT NULL REFERENCES avatar (id), "killer_id" INT NOT NULL REFERENCES avatar (id),
"victim_id" INT NOT NULL REFERENCES avatar (id),
"victim_exosuit" SMALLINT NOT NULL, "victim_exosuit" SMALLINT NOT NULL,
"victim_mounted" INT NOT NULL DEFAULT 0,
"weapon_id" SMALLINT NOT NULL, "weapon_id" SMALLINT NOT NULL,
"zone_id" SMALLINT NOT NULL, "zone_id" SMALLINT NOT NULL,
"px" INT NOT NULL, "px" INT NOT NULL,
@ -33,7 +34,7 @@ CREATE TABLE IF NOT EXISTS "kda" (
"avatar_id" INT NOT NULL REFERENCES avatar (id), "avatar_id" INT NOT NULL REFERENCES avatar (id),
"kills" INT NOT NULL DEFAULT 0, "kills" INT NOT NULL DEFAULT 0,
"deaths" INT NOT NULL DEFAULT 0, "deaths" INT NOT NULL DEFAULT 0,
"assists" INT NOT NULL DEFAULT 0, "revives" INT NOT NULL DEFAULT 0,
UNIQUE(avatar_id) UNIQUE(avatar_id)
); );
@ -42,7 +43,7 @@ CREATE TABLE IF NOT EXISTS "kdasession" (
"session_id" INT NOT NULL, "session_id" INT NOT NULL,
"kills" INT NOT NULL DEFAULT 0, "kills" INT NOT NULL DEFAULT 0,
"deaths" INT NOT NULL DEFAULT 0, "deaths" INT NOT NULL DEFAULT 0,
"assists" INT NOT NULL DEFAULT 0, "revives" INT NOT NULL DEFAULT 0,
UNIQUE(avatar_id, session_id) UNIQUE(avatar_id, session_id)
); );
@ -73,89 +74,144 @@ CREATE TABLE IF NOT EXISTS "legacykills" (
UNIQUE(avatar_id) UNIQUE(avatar_id)
); );
CREATE VIEW kdacampaign AS ( CREATE TABLE IF NOT EXISTS "machinedestroyed" (
SELECT "index" SERIAL PRIMARY KEY NOT NULL,
session.avatar_id, "avatar_id" INT NOT NULL REFERENCES avatar (id),
SUM(session.kills) AS kills, "weapon_id" INT NOT NULL,
SUM(session.deaths) AS deaths, "machine_type" INT NOT NULL,
SUM(session.assists) AS assists, "machine_faction" SMALLINT NOT NULL,
COUNT(session.avatar_id) AS numberOfSessions "hacked_faction" SMALLINT NOT NULL,
FROM ( "as_cargo" BOOLEAN NOT NULL,
SELECT avatar_id, session_id, kills, deaths, assists "zone_num" SMALLINT NOT NULL,
FROM kdasession "px" INT NOT NULL,
UNION ALL "py" INT NOT NULL,
SELECT avatar_id, 0, kills, deaths, assists "pz" INT NOT NULL,
FROM kda "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) AS session
LEFT JOIN kda
ON kda.avatar_id = session.avatar_id
GROUP BY session.avatar_id
); );
CREATE VIEW weaponstatcampaign AS ( CREATE TABLE IF NOT EXISTS "assistactivity" (
SELECT "index" SERIAL PRIMARY KEY NOT NULL,
weaponstat.avatar_id, "victim_id" INT NOT NULL REFERENCES avatar (id),
weaponstat.weapon_id, "attacker_id" INT NOT NULL REFERENCES avatar (id),
SUM(session.shots_fired) AS shots_fired, "weapon_id" SMALLINT NOT NULL,
SUM(session.shots_landed) AS shots_landed, "zone_id" SMALLINT NOT NULL,
SUM(session.kills) AS kills, "px" INT NOT NULL,
SUM(session.assists) AS assists, "py" INT NOT NULL,
COUNT(session.session_id) AS numberOfSessions "pz" INT NOT NULL,
FROM ( "exp" INT NOT NULL,
SELECT avatar_id, weapon_id, session_id, shots_fired, shots_landed, kills, assists "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
FROM weaponstatsession
UNION ALL
SELECT avatar_id, weapon_id, 0, shots_fired, shots_landed, kills, assists
FROM weaponstat
) AS session
LEFT JOIN weaponstat
ON weaponstat.avatar_id = session.avatar_id
GROUP BY weaponstat.avatar_id, weaponstat.weapon_id
); );
/* CREATE TABLE IF NOT EXISTS "supportactivity" (
Procedure for initializing and increasing the session number. "index" SERIAL PRIMARY KEY NOT NULL,
Always indexes a new session. "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
CREATE OR REPLACE PROCEDURE proc_sessionnumber_initAndOrIncrease "target_exosuit" SMALLINT NOT NULL, -- benefactor's exo-suit
(avatarId IN Int, number OUT Int) "interaction_type" SMALLINT NOT NULL, -- classification of support
AS "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
BEGIN "exp" INT NOT NULL,
SELECT (MAX(session_id,0)+1) INTO number "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
FROM sessionnumber );
WHERE avatar_id = avatarId;
INSERT INTO sessionnumber CREATE TABLE IF NOT EXISTS "ntuactivity" (
VALUES (avatarId, number); "avatar_id" INT NOT NULL REFERENCES avatar (id),
END; "zone_id" INT NOT NULL,
$$ LANGUAGE plpgsql; "building_id" INT NOT NULL,
"exp" INT NOT NULL,
UNIQUE(avatar_id, zone_id, building_id)
);
CREATE TABLE IF NOT EXISTS "buildingcapture" (
"index" SERIAL PRIMARY KEY NOT NULL,
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"zone_id" INT NOT NULL,
"building_id" INT NOT NULL,
"exp" INT NOT NULL,
"exp_type" CHAR(3) NOT NULL, /* bep, cep, llu */
"timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "expbuildingcapture" (
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"zone_id" INT NOT NULL,
"building_id" INT NOT NULL,
"captures" INT NOT NULL,
"bep" INT NOT NULL,
"cep" INT NOT NULL,
UNIQUE(avatar_id, zone_id, building_id)
);
CREATE TABLE IF NOT EXISTS "llubuildingcapture" (
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"zone_id" INT NOT NULL,
"building_id" INT NOT NULL,
"captures" INT NOT NULL,
"exp" INT NOT NULL,
UNIQUE(avatar_id, zone_id, building_id)
);
CREATE TABLE IF NOT EXISTS "progressiondebt" (
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"experience" INT NOT NULL DEFAULT 0,
UNIQUE(avatar_id)
);
/* /*
Procedure for initializing and increasing the session number. Procedure for initializing and increasing the session number.
Index for a new session only if last session was created more than one hour ago. Index for a new session only if last session was created more than one hour ago.
*/ */
CREATE OR REPLACE PROCEDURE proc_sessionnumber_initAndOrIncreasePerHour CREATE OR REPLACE FUNCTION proc_sessionnumber_test
(avatarId IN Int, number OUT Int, nextNumber OUT Int) (avatarId integer)
RETURNS integer
AS AS
$$ $$
DECLARE time TIMESTAMP; DECLARE time TIMESTAMP;
DECLARE number integer;
BEGIN BEGIN
SELECT MAX(session_id,0) INTO number SELECT MAX(session_id) INTO number
FROM sessionnumber FROM sessionnumber
WHERE avatar_id = avatarId; WHERE avatar_id = avatarId;
SELECT timestamp INTO time SELECT COALESCE(timestamp) INTO time
FROM sessionnumber FROM sessionnumber
WHERE avatar_id = avatarId AND session_id = number; WHERE avatar_id = avatarId AND session_id = number;
IF (CAST(CURRENT_TIMESTAMP AS FLOAT) > CAST(DATE_ADD('hour', 1, DATE_TRUNC('hour', time)) AS FLOAT)) THEN IF (time IS null) THEN
number := 0;
END IF;
RETURN number;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION proc_sessionnumber_initAndOrIncreasePerHour
(avatarId integer)
RETURNS integer
AS
$$
DECLARE time TIMESTAMP;
DECLARE number integer;
DECLARE nextNumber integer;
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
nextNumber := 1;
INSERT INTO sessionnumber
VALUES (avatarId, nextNumber);
ELSIF (CURRENT_TIMESTAMP > DATE_TRUNC('hour', time) + interval '1' hour) THEN
nextNumber := number + 1; nextNumber := number + 1;
INSERT INTO sessionnumber INSERT INTO sessionnumber
VALUES (avatarId, number); VALUES (avatarId, nextNumber);
ELSE ELSE
nextNumber := number; nextNumber := number;
UPDATE sessionnumber UPDATE sessionnumber
SET timestamp = CURRENT_TIMESTAMP SET timestamp = CURRENT_TIMESTAMP
WHERE avatar_id = avatarId AND session_id = number; WHERE avatar_id = avatarId AND session_id = number;
END IF; END IF;
RETURN nextNumber;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
@ -163,24 +219,35 @@ $$ LANGUAGE plpgsql;
Procedure for accessing any existing session number. Procedure for accessing any existing session number.
Actually polls the previous session number from the account table. Actually polls the previous session number from the account table.
*/ */
CREATE OR REPLACE PROCEDURE proc_sessionnumber_get CREATE OR REPLACE FUNCTION proc_sessionnumber_get
(avatarId IN Int, number OUT Int) (avatarId integer)
RETURNS integer
AS AS
$$ $$
DECLARE sessionId integer;
DECLARE number integer;
BEGIN BEGIN
SELECT COALESCE(session_id,0) INTO number SELECT COALESCE(session_id) INTO sessionId
FROM account FROM account
WHERE avatar_logged_in = avatarId; WHERE avatar_logged_in = avatarId;
IF sessionId IS NULL THEN
number := 0;
ELSE
number := sessionId;
END IF;
RETURN number;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
/* /*
Procedures for ensuring that row entries can be found in specified tables before some other DML operation updates those row entries. Procedures for ensuring that row entries can be found in specified tables before some other DML operation updates those row entries.
*/ */
CREATE OR REPLACE PROCEDURE proc_kda_addEntryIfNone CREATE OR REPLACE FUNCTION proc_kda_addEntryIfNone
(avatarId IN Int) (avatarId integer)
RETURNS integer
AS AS
$$ $$
DECLARE out integer;
BEGIN BEGIN
IF EXISTS( IF EXISTS(
SELECT * SELECT *
@ -189,43 +256,115 @@ BEGIN
HAVING COUNT(*) = 0) THEN HAVING COUNT(*) = 0) THEN
INSERT INTO kda (avatar_id) INSERT INTO kda (avatar_id)
VALUES (avatarId); VALUES (avatarId);
out := 1;
ELSE
out := 0;
END IF; END IF;
RETURN out;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE proc_weaponstat_addEntryIfNone CREATE OR REPLACE FUNCTION proc_weaponstat_addEntryIfNone
(avatarId IN Int, weaponId IN Int) (avatarId integer, weaponId integer)
RETURNS integer
AS AS
$$ $$
DECLARE sessionId Int; DECLARE out integer;
BEGIN BEGIN
SELECT proc_sessionnumber_get(avatarId, sessionId); IF NOT EXISTS(
IF EXISTS( SELECT avatar_id, weapon_id
SELECT *
FROM weaponstat FROM weaponstat
WHERE avatar_id = avatarId AND weapon_id = weaponId WHERE avatar_id = avatarId AND weapon_id = weaponId
HAVING COUNT(*) = 0) THEN GROUP BY avatar_id, weapon_id
INSERT INTO weaponstat (avatar_id, session_id, weapon_id) ) THEN
VALUES (avatarId, sessionId, weaponId); INSERT INTO weaponstat (avatar_id, weapon_id)
VALUES (avatarId, weaponId);
out := 1;
ELSE
out := 0;
END IF; END IF;
RETURN out;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE proc_weaponstatsession_addEntryIfNone CREATE OR REPLACE FUNCTION proc_weaponstatsession_addEntryIfNoneWithSessionId
(avatarId IN Int, weaponId IN Int) (avatarId integer, weaponId integer, sessionId integer)
RETURNS integer
AS
$$
DECLARE out integer;
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);
out := 1;
ELSE
out := 0;
END IF;
RETURN out;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION proc_weaponstatsession_addEntryIfNone
(avatarId integer, weaponId integer)
RETURNS integer
AS AS
$$ $$
DECLARE sessionId Int; DECLARE sessionId Int;
BEGIN BEGIN
SELECT proc_sessionnumber_get(avatarId, sessionId); sessionId := proc_sessionnumber_get(avatarId);
IF EXISTS( RETURN proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, weaponId, sessionId);
SELECT * END;
FROM weaponstatsession $$ LANGUAGE plpgsql;
WHERE avatar_id = avatarId AND weapon_id = weaponId
HAVING COUNT(*) = 0) THEN CREATE OR REPLACE FUNCTION proc_expbuildingcapture_addEntryIfNone
INSERT INTO weaponstatsession (avatar_id, session_id, weapon_id) (avatarId integer, buildingId integer, zoneId integer)
VALUES (avatarId, sessionId, weaponId); RETURNS integer
AS
$$
DECLARE out integer;
BEGIN
IF NOT EXISTS(
SELECT avatar_id, building_id, zone_id
FROM expbuildingcapture
WHERE avatar_id = avatarId AND building_id = buildingId AND zone_id = zoneId
GROUP BY avatar_id, building_id, zone_id
) THEN
INSERT INTO expbuildingcapture (avatar_id, building_id, zone_id, captures, bep, cep)
VALUES (avatarId, building_id, zone_id, 0, 0, 0);
out := 1;
ELSE
out := 0;
END IF; END IF;
RETURN out;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION proc_llubuildingcapture_addEntryIfNone
(avatarId integer, buildingId integer, zoneId integer)
RETURNS integer
AS
$$
DECLARE out integer;
BEGIN
IF NOT EXISTS(
SELECT avatar_id, building_id, zone_id
FROM llubuildingcapture
WHERE avatar_id = avatarId AND building_id = buildingId AND zone_id = zoneId
GROUP BY avatar_id, building_id, zone_id
) THEN
INSERT INTO llubuildingcapture (avatar_id, building_id, zone_id, captures, exp)
VALUES (avatarId, building_id, zone_id, 0, 0);
out := 1;
ELSE
out := 0;
END IF;
RETURN out;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
@ -243,11 +382,12 @@ DECLARE sessionId Int;
DECLARE oldSessionId Int; DECLARE oldSessionId Int;
BEGIN BEGIN
avatarId := NEW.avatar_logged_in; avatarId := NEW.avatar_logged_in;
SELECT proc_sessionnumber_initAndOrIncreasePerHour(avatarId, oldSessionId, sessionId); oldSessionId := proc_sessionnumber_test(avatarId);
sessionId := proc_sessionnumber_initAndOrIncreasePerHour(avatarId);
IF (sessionId > oldSessionId) THEN IF (sessionId > oldSessionId) THEN
BEGIN BEGIN
UPDATE account UPDATE account
SET sessionId = sessionId SET session_id = sessionId
WHERE id = OLD.id; WHERE id = OLD.id;
INSERT INTO kdasession (avatar_id, session_id) INSERT INTO kdasession (avatar_id, session_id)
VALUES (avatarId, sessionId); VALUES (avatarId, sessionId);
@ -258,7 +398,7 @@ END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER psf_account_newSession CREATE OR REPLACE TRIGGER psf_account_newSession
BEFORE UPDATE AFTER UPDATE
OF avatar_logged_in OF avatar_logged_in
ON account ON account
FOR EACH ROW FOR EACH ROW
@ -281,12 +421,13 @@ DECLARE victimSessionId Int;
DECLARE killerId Int; DECLARE killerId Int;
DECLARE victimId Int; DECLARE victimId Int;
DECLARE weaponId Int; DECLARE weaponId Int;
DECLARE out integer;
BEGIN BEGIN
killerId := NEW.killer_id; killerId := NEW.killer_id;
victimId := NEW.victim_id; victimId := NEW.victim_id;
weaponId := NEW.weapon_id; weaponId := NEW.weapon_id;
SELECT proc_sessionnumber_get(killerId, killerSessionId); killerSessionId := proc_sessionnumber_get(killerId);
SELECT proc_sessionnumber_get(victimId, victimSessionId); victimSessionId := proc_sessionnumber_get(victimId);
BEGIN BEGIN
UPDATE kdasession UPDATE kdasession
SET kills = kills + 1 SET kills = kills + 1
@ -298,6 +439,7 @@ BEGIN
WHERE avatar_id = victimId AND session_id = victimSessionId; WHERE avatar_id = victimId AND session_id = victimSessionId;
END; END;
BEGIN BEGIN
out := proc_weaponstatsession_addEntryIfNoneWithSessionId(killerId, weaponId, killerSessionId);
UPDATE weaponstatsession UPDATE weaponstatsession
SET kills = kills + 1 SET kills = kills + 1
WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId; WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId;
@ -323,10 +465,12 @@ AS
$$ $$
DECLARE avatarId Int; DECLARE avatarId Int;
DECLARE weaponId Int; DECLARE weaponId Int;
DECLARE out integer;
BEGIN BEGIN
avatarId := NEW.avatar_id; avatarId := NEW.avatar_id;
weaponId := NEW.weapon_id; weaponId := NEW.weapon_id;
SELECT proc_weaponstatsession_addEntryIfNone(avatarId, weaponId); out := proc_weaponstatsession_addEntryIfNone(avatarId, weaponId);
RETURN NEW;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
@ -346,9 +490,11 @@ RETURNS TRIGGER
AS AS
$$ $$
DECLARE avatarId Int; DECLARE avatarId Int;
DECLARE out integer;
BEGIN BEGIN
avatarId := NEW.avatar_id; avatarId := NEW.avatar_id;
SELECT proc_kda_addEntryIfNone(avatarId); out := proc_kda_addEntryIfNone(avatarId);
RETURN NEW;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
@ -369,10 +515,12 @@ AS
$$ $$
DECLARE avatarId Int; DECLARE avatarId Int;
DECLARE weaponId Int; DECLARE weaponId Int;
DECLARE out integer;
BEGIN BEGIN
avatarId := NEW.avatar_id; avatarId := NEW.avatar_id;
weaponId := NEW.weapon_id; weaponId := NEW.weapon_id;
SELECT proc_weaponstat_addEntryIfNone(avatarId, weaponId); out := proc_weaponstat_addEntryIfNone(avatarId, weaponId);
RETURN NEW;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
@ -382,10 +530,40 @@ ON weaponstat
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION fn_weaponstat_addEntryIfNone(); EXECUTE FUNCTION fn_weaponstat_addEntryIfNone();
/*
Before attempting to update the revival data in a character's session kda column,
set the session id to the current value then
manually update the column, overriding the process.
*/
CREATE OR REPLACE FUNCTION fn_kdasession_updateRevives()
RETURNS TRIGGER
AS
$$
DECLARE avatarId Int;
DECLARE sessionId Int;
DECLARE newRevives Int;
BEGIN
avatarId := NEW.avatar_id;
sessionId := proc_sessionnumber_get(avatarId);
newRevives := NEW.revives;
if (newRevives > 0) THEN
UPDATE kdasession
SET revives = revives + newRevives
WHERE avatar_id = avatarId AND session_id = sessionId;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER psf_kdasession_updateRevives
BEFORE INSERT
ON kdasession
FOR EACH ROW
EXECUTE FUNCTION fn_kdasession_updateRevives();
/* /*
Upon deletion of row entries for a character's session KDA, Upon deletion of row entries for a character's session KDA,
the values are copied over to the campaign KDA record for that character. the values are copied over to the campaign KDA record for that character.
This will fire mainly when called by the trigger for login (above).
*/ */
CREATE OR REPLACE FUNCTION fn_kdasession_updateOnDelete() CREATE OR REPLACE FUNCTION fn_kdasession_updateOnDelete()
RETURNS TRIGGER RETURNS TRIGGER
@ -394,16 +572,18 @@ $$
DECLARE avatarId Int; DECLARE avatarId Int;
DECLARE oldKills Int; DECLARE oldKills Int;
DECLARE oldDeaths Int; DECLARE oldDeaths Int;
DECLARE oldAssists Int; DECLARE oldRevives Int;
DECLARE out integer;
BEGIN BEGIN
avatarId := OLD.avatar_id; avatarId := OLD.avatar_id;
oldKills := OLD.kills; oldKills := OLD.kills;
oldDeaths := OLD.deaths; oldDeaths := OLD.deaths;
oldAssists := OLD.assists; oldRevives := OLD.Revives;
out := proc_kda_addEntryIfNone(avatarId);
UPDATE kda UPDATE kda
SET kills = kills + oldKills, SET kills = kills + oldKills,
deaths = deaths + oldDeaths, deaths = deaths + oldDeaths,
assists = assists + oldAssists revives = revives + oldRevives
WHERE avatar_id = avatarId; WHERE avatar_id = avatarId;
RETURN OLD; RETURN OLD;
END; END;
@ -418,7 +598,6 @@ EXECUTE FUNCTION fn_kdasession_updateOnDelete();
/* /*
Upon deletion of row entries for a character's session weapon stats, Upon deletion of row entries for a character's session weapon stats,
the values are copied over to the campaign weapon stats record for that character. the values are copied over to the campaign weapon stats record for that character.
This will fire mainly when called by the trigger for login (above).
*/ */
CREATE OR REPLACE FUNCTION fn_weaponstatsession_updateOnDelete() CREATE OR REPLACE FUNCTION fn_weaponstatsession_updateOnDelete()
RETURNS TRIGGER RETURNS TRIGGER
@ -430,6 +609,7 @@ DECLARE oldKills Int;
DECLARE oldAssists Int; DECLARE oldAssists Int;
DECLARE oldFired Int; DECLARE oldFired Int;
DECLARE oldLanded Int; DECLARE oldLanded Int;
DECLARE out integer;
BEGIN BEGIN
avatarId := OLD.avatar_id; avatarId := OLD.avatar_id;
weaponId := OLD.weapon_id; weaponId := OLD.weapon_id;
@ -437,12 +617,14 @@ BEGIN
oldAssists := OLD.assists; oldAssists := OLD.assists;
oldFired := OLD.shots_fired; oldFired := OLD.shots_fired;
oldLanded := OLD.shots_landed; oldLanded := OLD.shots_landed;
out := proc_weaponstat_addEntryIfNone(avatarId, weaponId);
UPDATE weaponstat UPDATE weaponstat
SET kills = kills + oldKills, SET kills = kills + oldKills,
assists = assists + oldAssists, assists = assists + oldAssists,
shots_fired = shots_fired + oldFired, shots_fired = shots_fired + oldFired,
shots_landed = shots_landed + oldLanded shots_landed = shots_landed + oldLanded
WHERE avatar_id = avatarId AND weapon_id = weaponId; WHERE avatar_id = avatarId AND weapon_id = weaponId;
RETURN OLD;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
@ -451,3 +633,152 @@ BEFORE DELETE
ON weaponstatsession ON weaponstatsession
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION fn_weaponstatsession_updateOnDelete(); EXECUTE FUNCTION fn_weaponstatsession_updateOnDelete();
/*
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;
DECLARE out integer;
BEGIN
avatarId := NEW.avatar_id;
weaponId := NEW.weapon_id;
oldAssists := NEW.assists;
oldFired := NEW.shots_fired;
oldLanded := NEW.shots_landed;
sessionId := proc_sessionnumber_get(avatarId);
out := proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, weaponId, sessionId);
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();
/*
A kill assist activity causes a major update to weapon stats:
the weapon that was used in the activity has the kills count for the killer updated/increased.
*/
CREATE OR REPLACE FUNCTION fn_assistactivity_updateRelatedStats()
RETURNS TRIGGER
AS
$$
DECLARE killerSessionId Int;
DECLARE killerId Int;
DECLARE weaponId Int;
DECLARE out integer;
BEGIN
killerId := NEW.killer_id;
weaponId := NEW.weapon_id;
killerSessionId := proc_sessionnumber_get(killerId);
out := proc_weaponstatsession_addEntryIfNone(killerId, killerSessionId, weaponId);
BEGIN
UPDATE weaponstatsession
SET assists = assists + 1
WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId;
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER psf_assistactivity_updateRelatedStats
AFTER INSERT
ON killactivity
FOR EACH ROW
EXECUTE FUNCTION fn_assistactivity_updateRelatedStats();
/*
Upon deletion of row entries for a character's building capture table,
the values are copied over to a total capture record for that character.
This will sort the values into one of two tables, and,
for one table, it will add to either one column or to a different column.
*/
CREATE OR REPLACE FUNCTION fn_buildingcapture_updateOnDelete()
RETURNS TRIGGER
AS
$$
DECLARE oldAvatarId Int;
DECLARE oldZoneId Int;
DECLARE oldBuildingId Int;
DECLARE oldExp Int;
DECLARE oldType CHAR(3);
DECLARE out integer;
BEGIN
oldAvatarId := NEW.avatar_id;
oldZoneId := NEW.zone_id;
oldBuildingId := NEW.building_id;
oldExp := NEW.exp;
oldType := NEW.exp_type;
BEGIN
IF (oldType LIKE "bep") THEN
out := proc_expbuildingcapture_addEntryIfNone(avatarId, buildingId, zoneId);
UPDATE expbuildingcapture
SET bep = bep + oldExp
WHERE avatar_id = oldAvatarId AND zone_id = oldZOneId AND building_id = oldBuildingId;
ELSIF (oldType LIKE "cep") THEN
out := proc_expbuildingcapture_addEntryIfNone(avatarId, buildingId, zoneId);
UPDATE expbuildingcapture
SET cep = cep + oldExp
WHERE avatar_id = oldAvatarId AND zone_id = oldZOneId AND building_id = oldBuildingId;
ELSIF (oldType LIKE "llu") THEN
out := proc_llubuildingcapture_addEntryIfNone(avatarId, buildingId, zoneId);
UPDATE llubuildingcapture
SET exp = exp + oldExp
WHERE avatar_id = oldAvatarId AND zone_id = oldZOneId AND building_id = oldBuildingId;
END IF;
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER psf_buildingcapture_updateOnDelete
BEFORE DELETE
ON buildingcapture
FOR EACH ROW
EXECUTE FUNCTION fn_buildingcapture_updateOnDelete();
/*
If a new avatar is created, a corresponding progression debt entry is also created by default.
*/
CREATE OR REPLACE FUNCTION fn_avatar_addDebtEntry()
RETURNS TRIGGER
AS
$$
DECLARE newAvatarId Int;
BEGIN
newAvatarId := NEW.avatar_id;
BEGIN
INSERT INTO progressiondebt (avatar_id)
VALUES (newAvatarId);
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER psf_avatar_addDebtEntry
AFTER INSERT
ON avatar
FOR EACH ROW
EXECUTE FUNCTION fn_avatar_addDebtEntry();

View file

@ -1,380 +0,0 @@
/* changes to objects from V008__Scoring.sql */
ALTER TABLE killactivity
ADD COLUMN victim_mounted INT NOT 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 TABLE IF NOT EXISTS 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
);
CREATE TABLE IF NOT EXISTS "assistactivity" (
"index" SERIAL PRIMARY KEY NOT NULL,
"victim_id" INT NOT NULL REFERENCES avatar (id),
"attacker_id" INT NOT NULL REFERENCES avatar (id),
"weapon_id" SMALLINT NOT NULL,
"zone_id" SMALLINT NOT NULL,
"px" INT NOT NULL,
"py" INT NOT NULL,
"pz" INT NOT NULL,
"exp" INT NOT NULL,
"timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS 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
"target_exosuit" SMALLINT NOT NULL, -- benefactor's exo-suit
"interaction_type" SMALLINT NOT NULL, -- classification 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
"exp" INT NOT NULL,
"timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS respawnsession (
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"session_id" INT NOT NULL,
"respawn_count" INT NOT NULL DEFAULT 0,
UNIQUE(avatar_id, session_id)
);
CREATE TABLE IF NOT EXISTS respawn (
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"respawn_count" INT NOT NULL DEFAULT 0,
UNIQUE(avatar_id)
);
/*
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();
/*
A kill assist activity causes a major update to weapon stats:
the weapon that was used in the activity has the kills count for the killer updated/increased.
*/
CREATE OR REPLACE FUNCTION fn_assistactivity_updateRelatedStats()
RETURNS TRIGGER
AS
$$
DECLARE killerSessionId Int;
DECLARE killerId Int;
DECLARE weaponId Int;
BEGIN
killerId := NEW.killer_id;
weaponId := NEW.weapon_id;
CALL proc_sessionnumber_get(killerId, killerSessionId);
BEGIN
UPDATE weaponstatsession
SET assists = assists + 1
WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId;
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER psf_assistactivity_updateRelatedStats
AFTER INSERT
ON killactivity
FOR EACH ROW
EXECUTE FUNCTION fn_assistactivity_updateRelatedStats();

View file

@ -231,17 +231,17 @@ object VehicleSpawnPadControlTest {
override def SetupNumberPools(): Unit = {} override def SetupNumberPools(): Unit = {}
} }
zone.GUID(guid) zone.GUID(guid)
zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.nanoTime()}") zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.currentTimeMillis()}")
// Hack: Wait for the Zone to finish booting, otherwise later tests will fail randomly due to race conditions // Hack: Wait for the Zone to finish booting, otherwise later tests will fail randomly due to race conditions
// with actor probe setting // with actor probe setting
// TODO(chord): Remove when Zone supports notification of booting being complete // TODO(chord): Remove when Zone supports notification of booting being complete
Thread.sleep(5000) Thread.sleep(5000)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.nanoTime()}") vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.currentTimeMillis()}")
val pad = VehicleSpawnPad(GlobalDefinitions.mb_pad_creation) val pad = VehicleSpawnPad(GlobalDefinitions.mb_pad_creation)
pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.nanoTime()}") pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.currentTimeMillis()}")
pad.Owner = pad.Owner =
new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building)
pad.Owner.Faction = faction pad.Owner.Faction = faction

View file

@ -75,15 +75,6 @@ game {
third-party = no third-party = no
} }
# Battle experience rate
bep-rate = 1.0
# Command experience rate
cep-rate = 1.0
# The maximum command experience that can be earned in a facility capture based on squad size
maximum-cep-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860]
# Modify the amount of mending per autorepair tick for facility amenities # Modify the amount of mending per autorepair tick for facility amenities
amenity-autorepair-rate = 1.0 amenity-autorepair-rate = 1.0
@ -226,6 +217,163 @@ game {
# Don't ask. # Don't ask.
doors-can-be-opened-by-med-app-from-this-distance = 5.05 doors-can-be-opened-by-med-app-from-this-distance = 5.05
# How the experience calculates
experience {
# The short contribution time when events are collected and evaluated.
short-contribution-time = 300000
# The long contribution time when events are collected and evaluated
# even factoring the same events from the short contribution time.
# As a result, when comparing the two event lists, similar actors may appear
# but their contributions may be different.
long-contribution-time = 600000
# Battle experience points
# BEP is to be calculated in relation to how valuable a kill is worth.
bep = {
# After all calculations are complete, multiple the result by this value
rate = 1.0
# These numbers are to determine the starting value for a particular kill
base = {
# Black Ops multiplies the base value by this much
bops-multiplier = 10.0
# If the player who died ever utilized a mechanized assault exo-suit
as-max = 250
# The player who died got at least one kill
with-kills = 100
# The player who died was mounted in a vehicle at the time of death
as-mounted = 100
# The player who died after having been in the game world for a while after spawning.
# Dying before this is often called a "spawn kill".
mature = 50
}
}
# Support experience points
# The events from which support experience rises are numerous.
# Calculation is determined by the selection of an "event" that decides how the values are combined.
sep = {
# After all calculations are complete, multiple the result by this value
rate = 1.0
# When using an advanced nanite transport to deposit into the resource silo of a major facility,
# for reaching the maximum amount of a single deposit,
# reward the user with this amount of support experience points.
# Small deposits reward only a percentage of this value.
ntu-silo-deposit-reward = 100
# When the event can not be found, this flat sum is rewarded.
# This should not be treated as a feature.
# It is a bug.
# Check your event label calls.
can-not-find-event-default-value = 15
# The events by which support experience calculation occurs.
# Events can be composed of three parts: a base value, a per-use (shots) value, and an active amount value.
# "Per-use" relies on knowledge from the server about the number of times this exact action occurred before the event.
# "Active amount" relies on knowledge from the server about how much of the changes for this event are still valid.
# Some changes can be undone by other events or other behavior.
#
# name - label by which this event is organized
# base - whole number value
# shots-multiplier - whether use count matters for this event
# - when set to 0.0 (default), it does not
# shots-limit - upper limit of use count
# - cap the count here, if higher
# shots-cutoff - if the use count exceeds this number, the event no longer applies
# - a hard limit that should zero the contribution reward
# - the *-cutoff should probably apply before *-limit, maybe
# shots-nat-log - when set, may the use count to a natural logarithmic curve
# - actually the exponent on the use count before the logarithm
# - similar to shots-limit, but the curve plateaus quickly
# amount-multiplier - whether active amount matters for this event
# - when set to 0.0 (default), it does not
events = [
{
name = "support-heal"
base = 10
shots-multiplier = 5.0
shots-limit = 100
amount-multiplier = 2.0
}
{
name = "support-repair"
base = 10
shots-multiplier = 5.0
shots-limit = 100
}
{
name = "support-repair-terminal"
base = 10
shots-multiplier = 5.0
shots-limit = 100
}
{
name = "support-repair-turret"
base = 10
shots-multiplier = 5.0
shots-limit = 100
}
{
name = "mounted-kill"
base = 25
}
{
name = "router"
base = 15
}
{
name = "hotdrop"
base = 25
}
{
name = "hack"
base = 5
amount-multiplier = 5.0
}
{
name = "ams-resupply"
base = 15
shots-multiplier = 1.0
shots-nat-log = 5.0
}
{
name = "lodestar-repair"
base = 10
shots-multiplier = 1.0
shots-nat-log = 5.0
shots-limit = 100
amount-multiplier = 1.0
}
{
name = "lodestar-rearm"
base = 10
shots-multiplier = 1.0
shots-nat-log = 5.0
}
{
name = "revival"
base = 0
shots-multiplier = 15.0
shots-nat-log = 5.0
shots-cutoff = 10
}
]
}
# Support experience points
cep = {
# After all calculations are complete, multiple the result by this value
rate = 1.0
# When command experience points are rewarded to the lattice link unit carrier,
# modify the original value by this modifier.
llu-carrier-modifier = 0.5
# The maximum command experience that can be earned in a facility capture based on squad size
maximum-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860]
# When the cep has to be capped for squad size, add a small value to the capped value
# -1 reuses the cep before being capped
squad-size-limit-overflow = -1
# When the cep has to be capped for squad size, calculate a small amount to add to the capped value
squad-size-limit-overflow-multiplier = 0.2
}
}
} }
anti-cheat { anti-cheat {

View file

@ -851,12 +851,18 @@ object AvatarActor {
) )
} }
def updateToolDischargeFor(avatarId: Long, lives: Seq[Life]): Unit = { def updateToolDischargeFor(avatar: Avatar): Unit = {
lives updateToolDischargeFor(avatar.id, avatar.scorecard.CurrentLife)
.flatMap { _.equipmentStats } }
.foreach { stat =>
zones.exp.ToDatabase.reportToolDischarge(avatarId, stat) def updateToolDischargeFor(avatarId: Long, life: Life): Unit = {
} updateToolDischargeFor(avatarId, life.equipmentStats)
}
def updateToolDischargeFor(avatarId: Long, stats: Seq[EquipmentStat]): Unit = {
stats.foreach { stat =>
zones.exp.ToDatabase.reportToolDischarge(avatarId, stat)
}
} }
def toAvatar(avatar: persistence.Avatar): Avatar = { def toAvatar(avatar: persistence.Avatar): Avatar = {
@ -1091,7 +1097,6 @@ class AvatarActor(
.receiveSignal { .receiveSignal {
case (_, PostStop) => case (_, PostStop) =>
AvatarActor.avatarNoLongerLoggedIn(account.id) AvatarActor.avatarNoLongerLoggedIn(account.id)
AvatarActor.updateToolDischargeFor(avatar.id.toLong, avatar.scorecard.CurrentLife +: avatar.scorecard.Lives)
Behaviors.same Behaviors.same
} }
} }
@ -1863,6 +1868,7 @@ class AvatarActor(
AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool) AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool)
} }
saveLockerFunc() saveLockerFunc()
AvatarActor.updateToolDischargeFor(avatar)
Behaviors.same Behaviors.same
} }
} }
@ -3084,6 +3090,7 @@ class AvatarActor(
} }
def updateDeaths(deathStat: Death): Unit = { def updateDeaths(deathStat: Death): Unit = {
AvatarActor.updateToolDischargeFor(avatar)
avatar.scorecard.rate(deathStat) avatar.scorecard.rate(deathStat)
val _session = session.get val _session = session.get
val zone = _session.zone val zone = _session.zone
@ -3138,13 +3145,13 @@ class AvatarActor(
def updateExperienceAndType(exp: Long): (Long, ExperienceType) = { def updateExperienceAndType(exp: Long): (Long, ExperienceType) = {
val _session = session.get val _session = session.get
val player = _session.player val player = _session.player
val gameOpts = Config.app.game val gameOpts = Config.app.game.experience.bep
val (modifier, msg) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) { val (modifier, msg) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) {
(1.25f, ExperienceType.RabbitBall) (1.25f, ExperienceType.RabbitBall)
} else { } else {
(1f, ExperienceType.Normal) (1f, ExperienceType.Normal)
} }
((exp * modifier * gameOpts.bepRate).toLong, msg) ((exp * modifier * gameOpts.rate).toLong, msg)
} }
def updateToolDischarge(stats: EquipmentStat): Unit = { def updateToolDischarge(stats: EquipmentStat): Unit = {

View file

@ -5,6 +5,7 @@ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.objects.zones.exp
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@ -225,8 +226,6 @@ class SessionAvatarHandlers(
case AvatarResponse.DestroyDisplay(killer, victim, method, unk) case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
if killer.CharId == avatar.id && killer.Faction != victim.Faction => if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk)) sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
//TODO Temporary thing that should go somewhere else and use proper xp values
// avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong)
case AvatarResponse.Destroy(victim, killer, weapon, pos) => case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
// guid = victim // killer = killer // guid = victim // killer = killer
@ -398,71 +397,68 @@ class SessionAvatarHandlers(
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) => case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda) avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
case AvatarResponse.AwardBep(_, bep, expType) => case AvatarResponse.AwardBep(charId, bep, expType) =>
avatarActor ! AvatarActor.AwardBep(bep, expType) if (charId == player.CharId) {
avatarActor ! AvatarActor.AwardBep(bep, expType)
}
case AvatarResponse.AwardCep(0, cep) => case AvatarResponse.AwardCep(charId, cep) =>
//if the target player, always award (some) CEP
if (charId == player.CharId) {
avatarActor ! AvatarActor.AwardCep(cep)
}
case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
//must be in a squad to earn experience //must be in a squad to earn experience
val id = player.CharId val cepConfig = Config.app.game.experience.cep
val charId = player.CharId
val squadUI = sessionData.squad.squadUI val squadUI = sessionData.squad.squadUI
val participation = continent val participation = continent
.Buildings .Building(buildingId)
.values .map { building =>
.collect { case building if {
val soi = building.Definition.SOIRadius * building.Definition.SOIRadius
val pos = player.Position.xy
Vector3.DistanceSquared(building.Position.xy, pos) < soi
} =>
building.Participation.PlayerContribution() building.Participation.PlayerContribution()
} }
squadUI squadUI
.find { _._1 == id } .find { _._1 == charId }
.collect { .collect {
case (_, elem) if elem.index == 0 => case (_, elem) if elem.index == 0 =>
//squad leader earns CEP, modified by squad effort //squad leader earns CEP, modified by squad effort, capped by squad size present during the capture
val maxRate: Long = { val squadParticipation = participation match {
val maxCepList = Config.app.game.maximumCepPerSquadSize case Some(map) => map.filter { case (id, _) => squadUI.contains(id) }
val squadSize: Int = { case _ => Map.empty[Long, Float]
val squadSizeList: Iterable[Int] = participation }
.map { facilityMap => val maxCepBySquadSize: Long = {
squadUI.count { case (id, _) => facilityMap.contains(id) } val maxCepList = cepConfig.maximumPerSquadSize
} val squadSize: Int = squadParticipation.size
if (squadSizeList.nonEmpty) {
squadSizeList.max
} else {
0
}
}
maxCepList.lift(squadSize - 1).getOrElse(squadSize * maxCepList.head).toLong maxCepList.lift(squadSize - 1).getOrElse(squadSize * maxCepList.head).toLong
} }
val groupContribution: Float = { val groupContribution: Float = squadUI
val eachSquadMemberParticipation: Iterable[Float] = participation.map { facilityMap => .map { case (id, _) => (id, squadParticipation.getOrElse(id, 0f) / 10f) }
val foundSquadMemberParticipation: Iterable[Float] = squadUI .values
.keys .max
.flatMap { facilityMap.get } val modifiedExp: Long = (cep.toFloat * groupContribution).toLong
if (foundSquadMemberParticipation.nonEmpty) { val cappedModifiedExp: Long = math.min(modifiedExp, maxCepBySquadSize)
foundSquadMemberParticipation.sum / 10f val finalExp: Long = if (modifiedExp > cappedModifiedExp) {
} else { val overLimitOverflow = if (cepConfig.squadSizeLimitOverflow == -1) {
0f cep.toFloat
}
}
if (eachSquadMemberParticipation.nonEmpty) {
eachSquadMemberParticipation.max
} else { } else {
0 cepConfig.squadSizeLimitOverflow.toFloat
} }
cappedModifiedExp + (overLimitOverflow * cepConfig.squadSizeLimitOverflowMultiplier).toLong
} else {
cappedModifiedExp
} }
val modifiedExp: Long = math.min((cep.toFloat * groupContribution).toLong, maxRate) exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, finalExp, expType="cep")
avatarActor ! AvatarActor.AwardCep(modifiedExp) avatarActor ! AvatarActor.AwardCep(finalExp)
Some(modifiedExp) Some(finalExp)
case _ => case _ =>
//squad member earns BEP based on CEP, modified by personal effort //squad member earns BEP based on CEP, modified by personal effort
val individualContribution = { val individualContribution = {
val contributionList = for { val contributionList = for {
facilityMap <- participation facilityMap <- participation
if facilityMap.contains(id) if facilityMap.contains(charId)
} yield facilityMap(id) } yield facilityMap(charId)
if (contributionList.nonEmpty) { if (contributionList.nonEmpty) {
contributionList.max contributionList.max
} else { } else {
@ -470,26 +466,11 @@ class SessionAvatarHandlers(
} }
} }
val modifiedExp = (cep * individualContribution).toLong val modifiedExp = (cep * individualContribution).toLong
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep")
avatarActor ! AvatarActor.AwardBep(modifiedExp, ExperienceType.Normal) avatarActor ! AvatarActor.AwardBep(modifiedExp, ExperienceType.Normal)
Some(modifiedExp) Some(modifiedExp)
} }
case AvatarResponse.AwardCep(charId, cep) =>
//if the target player, always award (some) CEP
val squadUI = sessionData.squad.squadUI
if (charId == player.CharId) {
val maxRate: Long = squadUI.find { _._1 == avatar.id } match {
case Some((_, elem)) if elem.index == 0 =>
val thisZone = continent.Number
val squadSize = squadUI.count { case (_, e) => e.zone == thisZone } - 1
val maxCepList = Config.app.game.maximumCepPerSquadSize
maxCepList.lift(squadSize).getOrElse(squadSize * maxCepList.head).toLong
case _ =>
Config.app.game.maximumCepPerSquadSize.head.toLong
}
avatarActor ! AvatarActor.AwardCep(math.min(cep, maxRate))
}
case AvatarResponse.SendResponse(msg) => case AvatarResponse.SendResponse(msg) =>
sendResponse(msg) sendResponse(msg)
@ -526,6 +507,7 @@ class SessionAvatarHandlers(
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
//player state changes //player state changes
AvatarActor.updateToolDischargeFor(avatar)
player.FreeHand.Equipment.foreach { item => player.FreeHand.Equipment.foreach { item =>
DropEquipmentFromInventory(player)(item) DropEquipmentFromInventory(player)(item)
} }
@ -540,9 +522,7 @@ class SessionAvatarHandlers(
} }
sessionData.playerActionsToCancel() sessionData.playerActionsToCancel()
sessionData.terminals.CancelAllProximityUnits() sessionData.terminals.CancelAllProximityUnits()
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

View file

@ -2458,9 +2458,9 @@ class SessionData(
src: PlanetSideGameObject with TelepadLike, src: PlanetSideGameObject with TelepadLike,
dest: PlanetSideGameObject with TelepadLike dest: PlanetSideGameObject with TelepadLike
): Unit = { ): Unit = {
val time = System.nanoTime val time = System.currentTimeMillis()
if ( if (
time - recentTeleportAttempt > (2 seconds).toNanos && router.DeploymentState == DriveState.Deployed && time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
internalTelepad.Active && internalTelepad.Active &&
remoteTelepad.Active remoteTelepad.Active
) { ) {

View file

@ -7,6 +7,7 @@ import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import net.psforever.login.WorldSession import net.psforever.login.WorldSession
import net.psforever.objects.avatar.scoring.ScoreCard
import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.mount.Seat import net.psforever.objects.serverobject.mount.Seat
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
@ -2342,6 +2343,7 @@ class ZoningOperations(
player.avatar = player.avatar.copy(stamina = avatar.maxStamina) player.avatar = player.avatar.copy(stamina = avatar.maxStamina)
avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina) avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina)
avatarActor ! AvatarActor.ResetImplants() avatarActor ! AvatarActor.ResetImplants()
zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife))
val obj = Player.Respawn(tplayer) val obj = Player.Respawn(tplayer)
DefinitionUtil.applyDefaultLoadout(obj) DefinitionUtil.applyDefaultLoadout(obj)
obj.death_by = tplayer.death_by obj.death_by = tplayer.death_by

View file

@ -9847,6 +9847,7 @@ object GlobalDefinitions {
resource_silo.Damageable = false resource_silo.Damageable = false
resource_silo.Repairable = false resource_silo.Repairable = false
resource_silo.MaxNtuCapacitor = 1000 resource_silo.MaxNtuCapacitor = 1000
resource_silo.ChargeTime = 105.seconds //from 0-100% in roughly 105s on live (~20%-100% https://youtu.be/veOWToR2nSk?t=1402)
capture_terminal.Name = "capture_terminal" capture_terminal.Name = "capture_terminal"
capture_terminal.Damageable = false capture_terminal.Damageable = false
@ -9856,7 +9857,7 @@ object GlobalDefinitions {
secondary_capture.Name = "secondary_capture" secondary_capture.Name = "secondary_capture"
secondary_capture.Damageable = false secondary_capture.Damageable = false
secondary_capture.Repairable = false secondary_capture.Repairable = false
secondary_capture.FacilityHackTime = 1.nanosecond secondary_capture.FacilityHackTime = 1.millisecond
vanu_control_console.Name = "vanu_control_console" vanu_control_console.Name = "vanu_control_console"
vanu_control_console.Damageable = false vanu_control_console.Damageable = false

View file

@ -110,6 +110,7 @@ class Player(var avatar: Avatar)
Health = Definition.DefaultHealth Health = Definition.DefaultHealth
Armor = MaxArmor Armor = MaxArmor
Capacitor = 0 Capacitor = 0
avatar.scorecard.respawn()
released = false released = false
} }
isAlive isAlive
@ -124,13 +125,16 @@ class Player(var avatar: Avatar)
def Revive: Boolean = { def Revive: Boolean = {
Destroyed = false Destroyed = false
Health = Definition.DefaultHealth Health = Definition.DefaultHealth
avatar.scorecard.revive()
released = false released = false
true true
} }
def Release: Boolean = { def Release: Boolean = {
released = true if (!released) {
backpack = !isAlive released = true
backpack = !isAlive
}
true true
} }
@ -626,6 +630,7 @@ object Player {
if (player.Release) { if (player.Release) {
val obj = new Player(player.avatar) val obj = new Player(player.avatar)
obj.Continent = player.Continent obj.Continent = player.Continent
obj.avatar.scorecard.respawn()
obj obj
} else { } else {
player player

View file

@ -99,7 +99,7 @@ class Tool(private val toolDef: ToolDefinition)
} }
def Discharge(rounds: Option[Int] = None): Int = { def Discharge(rounds: Option[Int] = None): Int = {
lastDischarge = System.nanoTime() lastDischarge = System.currentTimeMillis()
Magazine = FireMode.Discharge(this, rounds) Magazine = FireMode.Discharge(this, rounds)
} }

View file

@ -6,11 +6,14 @@ final case class Life(
assists: Seq[Assist], assists: Seq[Assist],
death: Option[Death], death: Option[Death],
equipmentStats: Seq[EquipmentStat], equipmentStats: Seq[EquipmentStat],
supportExperience: Long supportExperience: Long,
prior: Option[Life]
) )
object Life { object Life {
def apply(): Life = Life(Nil, Nil, None, Nil, 0) def apply(): Life = Life(Nil, Nil, None, Nil, 0, None)
def revive(prior: Life): Life = Life(Nil, Nil, None, Nil, 0, Some(prior))
def bep(life: Life): Long = { def bep(life: Life): Long = {
life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned) life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned)

View file

@ -18,6 +18,8 @@ class ScoreCard() {
def Lives: Seq[Life] = lives def Lives: Seq[Life] = lives
def AllLives: Seq[Life] = curr +: lives
def Kills: Seq[Kill] = lives.flatMap { _.kills } ++ curr.kills def Kills: Seq[Kill] = lives.flatMap { _.kills } ++ curr.kills
def KillStatistics: Map[Int, Statistic] = killStatistics.toMap def KillStatistics: Map[Int, Statistic] = killStatistics.toMap
@ -39,17 +41,41 @@ class ScoreCard() {
ScoreCard.updateStatisticsFor(assistStatistics, wid.equipment, faction) ScoreCard.updateStatisticsFor(assistStatistics, wid.equipment, faction)
} }
case d: Death => case d: Death =>
val expired = curr curr = curr.copy(death = Some(d))
curr = Life()
lives = expired.copy(death = Some(d)) +: lives
case value: Long => case value: Long =>
curr = curr.copy(supportExperience = curr.supportExperience + value) curr = curr.copy(supportExperience = curr.supportExperience + value)
case _ => () case _ => ()
} }
} }
def revive(): Unit = {
curr = Life.revive(curr)
}
def respawn(): Unit = {
val death = curr
curr = Life()
lives = death +: lives
}
} }
object ScoreCard { object ScoreCard {
def respawnCount(card: ScoreCard): Int = {
card.AllLives.foldLeft(0)(_ + recursiveReviveCount(_, count = 0))
}
def reviveCount(life: Life): Int = {
recursiveReviveCount(life, count = 0)
}
@tailrec
private def recursiveReviveCount(life: Life, count: Int): Int = {
life.prior match {
case None => count
case Some(previousLife) => recursiveReviveCount(previousLife, count + 1)
}
}
private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = { private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = {
updateEquipmentStat(curr, entry, entry.objectId, entry.kills, entry.assists) updateEquipmentStat(curr, entry, entry.objectId, entry.kills, entry.assists)
} }

View file

@ -17,7 +17,7 @@ class CaptureFlagConverter extends ObjectCreateConverter[CaptureFlag]() {
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L) case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L)
} }
val millisecondsRemaining = TimeUnit.MILLISECONDS.convert(math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.nanoTime), TimeUnit.NANOSECONDS) val millisecondsRemaining = math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.currentTimeMillis())
Success( Success(
CaptureFlagData( CaptureFlagData(

View file

@ -25,14 +25,14 @@ trait Hackable {
def HackedBy_=(agent: Option[Player]): Option[HackInfo] = { def HackedBy_=(agent: Option[Player]): Option[HackInfo] = {
(hackedBy, agent) match { (hackedBy, agent) match {
case (None, Some(actor)) => case (None, Some(actor)) =>
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L)) hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.currentTimeMillis(), 0L))
case (Some(info), Some(actor)) => case (Some(info), Some(actor)) =>
if (actor.Faction == this.Faction) { if (actor.Faction == this.Faction) {
//hack cleared //hack cleared
hackedBy = None hackedBy = None
} else if (actor.Faction != info.hackerFaction) { } else if (actor.Faction != info.hackerFaction) {
//override the hack state with a new hack state if the new user has different faction affiliation //override the hack state with a new hack state if the new user has different faction affiliation
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L)) hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.currentTimeMillis(), 0L))
} }
case (_, None) => case (_, None) =>
hackedBy = None hackedBy = None

View file

@ -7,11 +7,13 @@ import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.transfer.TransferBehavior import net.psforever.objects.serverobject.transfer.TransferBehavior
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.zones
import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior, Vehicle} import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior, Vehicle}
import net.psforever.types.{ExperienceType, PlanetSideEmpire} import net.psforever.types.{ExperienceType, PlanetSideEmpire}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.util.Config
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -188,11 +190,16 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
.map { v => (v, v.Owners) } .map { v => (v, v.Owners) }
.collect { case (vehicle, Some(owner)) => .collect { case (vehicle, Some(owner)) =>
//experience is reported as normal //experience is reported as normal
val deposit: Long = 100L * math.floor(amount).toLong / math.floor(resourceSilo.MaxNtuCapacitor / 105f).toLong val deposit: Long =
(Config.app.game.experience.sep.ntuSiloDepositReward.toFloat *
math.floor(amount).toFloat /
math.floor(resourceSilo.MaxNtuCapacitor / resourceSilo.Definition.ChargeTime.toMillis.toFloat)
).toLong
vehicle.Zone.AvatarEvents ! AvatarServiceMessage( vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
owner.name, owner.name,
AvatarAction.AwardBep(0, deposit, ExperienceType.Normal) AvatarAction.AwardBep(0, deposit, ExperienceType.Normal)
) )
zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit)
} }
} }
} }

View file

@ -4,12 +4,16 @@ package net.psforever.objects.serverobject.resourcesilo
import net.psforever.objects.NtuContainerDefinition import net.psforever.objects.NtuContainerDefinition
import net.psforever.objects.serverobject.structures.AmenityDefinition import net.psforever.objects.serverobject.structures.AmenityDefinition
import scala.concurrent.duration._
/** /**
* The definition for any `Resource Silo`. * The definition for any `Resource Silo`.
* Object Id 731. * Object Id 731.
*/ */
class ResourceSiloDefinition extends AmenityDefinition(731) class ResourceSiloDefinition extends AmenityDefinition(731)
with NtuContainerDefinition { with NtuContainerDefinition {
var ChargeTime: FiniteDuration = 0.seconds
Name = "resource_silo" Name = "resource_silo"
MaxNtuCapacitor = 1000 MaxNtuCapacitor = 1000
} }

View file

@ -183,8 +183,7 @@ class Building(
case Some(obj: CaptureTerminal with Hackable) => case Some(obj: CaptureTerminal with Hackable) =>
obj.HackedBy match { obj.HackedBy match {
case Some(Hackable.HackInfo(p, _, start, length)) => case Some(Hackable.HackInfo(p, _, start, length)) =>
val hack_time_remaining_ms = val hack_time_remaining_ms = math.max(0, start + length - System.currentTimeMillis())
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
(true, p.Faction, hack_time_remaining_ms) (true, p.Faction, hack_time_remaining_ms)
case _ => case _ =>
(false, PlanetSideEmpire.NEUTRAL, 0L) (false, PlanetSideEmpire.NEUTRAL, 0L)

View file

@ -11,11 +11,12 @@ import scala.collection.mutable
trait FacilityHackParticipation extends ParticipationLogic { trait FacilityHackParticipation extends ParticipationLogic {
protected var lastInfoRequest: Long = 0L protected var lastInfoRequest: Long = 0L
protected var infoRequestOverTime: Seq[Long] = Seq[Long]() protected var infoRequestOverTime: Seq[Long] = Seq[Long]()
/* /**
key: unique player identifier key: unique player identifier<br>
values: last player entry, number of times updated, time of last update (POSIX time)
*/ */
protected var playerContribution: mutable.LongMap[(Player, Int, Long)] = mutable.LongMap[(Player, Int, Long)]() protected var playerContribution: mutable.LongMap[(Player, Int, Long)] = mutable.LongMap[(Player, Int, Long)]()
protected var playerPopulationOverTime: Seq[Map[PlanetSideEmpire.Value, Int]] = Seq[ Map[PlanetSideEmpire.Value, Int]]() protected var playerPopulationOverTime: Seq[Map[PlanetSideEmpire.Value, Int]] = Seq[Map[PlanetSideEmpire.Value, Int]]()
def PlayerContribution(timeDelay: Long): Map[Long, Float] = { def PlayerContribution(timeDelay: Long): Map[Long, Float] = {
playerContribution playerContribution
@ -49,6 +50,13 @@ trait FacilityHackParticipation extends ParticipationLogic {
} }
} }
/**
* Eliminate participation for players who have no submitted updates within the time period.
* @param list current list of players
* @param now current time (ms)
* @param before how long before the current time beyond which players should be eliminated (ms)
* @see `timeSensitiveFilterAndAppend`
*/
protected def updatePopulationOverTime(list: List[Player], now: Long, before: Long): Unit = { protected def updatePopulationOverTime(list: List[Player], now: Long, before: Long): Unit = {
var populationList = list var populationList = list
val layer = PlanetSideEmpire.values.map { faction => val layer = PlanetSideEmpire.values.map { faction =>
@ -56,70 +64,82 @@ trait FacilityHackParticipation extends ParticipationLogic {
populationList = everyoneElse populationList = everyoneElse
(faction, isFaction.size) (faction, isFaction.size)
}.toMap[PlanetSideEmpire.Value, Int] }.toMap[PlanetSideEmpire.Value, Int]
playerPopulationOverTime = timeSensitiveFilterAndAppend(playerPopulationOverTime, layer, now, before) playerPopulationOverTime = timeSensitiveFilterAndAppend(playerPopulationOverTime, layer, now - before)
} }
protected def updateTime(now: Long): Unit = {
infoRequestOverTime = timeSensitiveFilterAndAppend(infoRequestOverTime, now, now - 900000L)
}
/**
* Eliminate entries from the primary input list based on time entries in a secondary time record list.
* The time record list must be updated independently.
* @param list input list whose entries are edited against time and then is appended
* @param newEntry new entry of the appropriate type to append to the end of the output list
* @param beforeTime how long before the current time beyond which entries in the input list should be eliminated (ms)
* @tparam T it does not matter what the type is
* @return the modified list
*/
protected def timeSensitiveFilterAndAppend[T]( protected def timeSensitiveFilterAndAppend[T](
list: Seq[T], list: Seq[T],
newEntry: T, newEntry: T,
now: Long = System.currentTimeMillis(), beforeTime: Long
before: Long
): Seq[T] = { ): Seq[T] = {
infoRequestOverTime match { infoRequestOverTime match {
case Nil => case Nil => Seq(newEntry)
Seq(newEntry)
case _ => case _ =>
val beforeTime = now - before
(infoRequestOverTime.indexWhere { _ >= beforeTime } match { (infoRequestOverTime.indexWhere { _ >= beforeTime } match {
case -1 => case -1 => list
list case cutOffIndex => list.drop(cutOffIndex)
case cutOffIndex =>
list.drop(cutOffIndex)
}) :+ newEntry }) :+ newEntry
} }
} }
} }
object FacilityHackParticipation { object FacilityHackParticipation {
private[participation] def calculateExperienceFromKills( private[participation] def allocateKillsByPlayers(
center: Vector3, center: Vector3,
radius: Float, radius: Float,
hackStart: Long, hackStart: Long,
completionTime: Long, completionTime: Long,
opposingFaction: PlanetSideEmpire.Value, opposingFaction: PlanetSideEmpire.Value,
contributionVictor: Iterable[(Player, Int, Long)], contributionVictor: Iterable[(Player, Int, Long)],
contributionOpposingSize: Int ): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
): Long = {
val killMapFunc: Iterable[(Player, Int, Long)] => Iterable[(UniquePlayer, Float, Seq[Kill])] = { val killMapFunc: Iterable[(Player, Int, Long)] => Iterable[(UniquePlayer, Float, Seq[Kill])] = {
killsEarnedPerPlayerDuringHack(center.xy, radius * radius, hackStart, hackStart + completionTime, opposingFaction) killsEarnedPerPlayerDuringHack(center.xy, radius * radius, hackStart, hackStart + completionTime, opposingFaction)
} }
val killMapValues = killMapFunc(contributionVictor) killMapFunc(contributionVictor)
val totalExperienceFromKills = killMapValues.flatMap { _._3.map { _.experienceEarned } }.sum
val experienceModifier = {
if (contributionOpposingSize > 0 && contributionOpposingSize < 10) {
contributionOpposingSize * 0.1f + math.random()
} else {
contributionOpposingSize * 0.1f
}
}
(totalExperienceFromKills * experienceModifier).toLong
} }
private def killsEarnedPerPlayerDuringHack( private[participation] def calculateExperienceFromKills(
centerXY: Vector3, killMapValues: Iterable[(UniquePlayer, Float, Seq[Kill])],
distanceSq: Float, contributionOpposingSize: Int
start: Long, ): Long = {
end: Long, val totalExperienceFromKills = killMapValues
faction: PlanetSideEmpire.Value .flatMap { _._3.map { _.experienceEarned } }
) .sum
( .toFloat
list: Iterable[(Player, Int, Long)] (totalExperienceFromKills * contributionOpposingSize.toFloat * 0.1d).toLong
): Iterable[(UniquePlayer, Float, Seq[Kill])] = { }
private[participation] def killsEarnedPerPlayerDuringHack(
centerXY: Vector3,
distanceSq: Float,
start: Long,
end: Long,
faction: PlanetSideEmpire.Value
)
(
list: Iterable[(Player, Int, Long)]
): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
val duration = end - start val duration = end - start
list.map { case (p, d, _) => list.map { case (p, d, _) =>
val killList = p.avatar.scorecard.Kills.filter { k => val killList = p.avatar.scorecard.Kills.filter { k =>
val killTime = k.info.interaction.hitTime val killTime = k.info.interaction.hitTime
k.victim.Faction == faction && start < killTime && killTime < end && Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq k.victim.Faction == faction &&
start < killTime &&
killTime <= end &&
Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq
} }
(PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList) (PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList)
} }
@ -220,7 +240,8 @@ object FacilityHackParticipation {
victorPop <- victorPopulationNumbers victorPop <- victorPopulationNumbers
opposePop <- opposingPopulationNumbers opposePop <- opposingPopulationNumbers
out = if ( out = if (
(opposePop < victorPop && opposePop * healthyPercentage > victorPop) || (opposePop + victorPop < 8) ||
(opposePop < victorPop && opposePop * healthyPercentage > victorPop) ||
(opposePop > victorPop && victorPop * healthyPercentage > opposePop) (opposePop > victorPop && victorPop * healthyPercentage > opposePop)
) { ) {
1f //balanced enough population 1f //balanced enough population
@ -241,7 +262,7 @@ object FacilityHackParticipation {
overwhelmingOddsBonus: Long overwhelmingOddsBonus: Long
): Long = { ): Long = {
if (opposingSize * steamrollPercentage < victorSize.toFloat) { if (opposingSize * steamrollPercentage < victorSize.toFloat) {
-steamrollBonus * (victorSize - opposingSize) //steamroll by the victor 0L //steamroll by the victor
} else if (victorSize * overwhelmingOddsPercentage <= opposingSize.toFloat) { } else if (victorSize * overwhelmingOddsPercentage <= opposingSize.toFloat) {
overwhelmingOddsBonus + opposingSize + victorSize //victory against overwhelming odds overwhelmingOddsBonus + opposingSize + victorSize //victory against overwhelming odds
} else { } else {

View file

@ -1,15 +1,17 @@
// Copyright (c) 2023 PSForever // Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.{Building, StructureType}
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector} import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@ -49,17 +51,13 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
requestLayers.completeWith(request) requestLayers.completeWith(request)
request.onComplete { request.onComplete {
case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) => case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) =>
hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis(), before = 900000L) hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis() - 900000L)
case _ => case _ =>
requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil))) requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil)))
} }
requestLayers.future requestLayers.future
} }
private def updateTime(now: Long): Unit = {
infoRequestOverTime = timeSensitiveFilterAndAppend(infoRequestOverTime, now, now, before = 900000L)
}
def RewardFacilityCapture( def RewardFacilityCapture(
defenderFaction: PlanetSideEmpire.Value, defenderFaction: PlanetSideEmpire.Value,
attackingFaction: PlanetSideEmpire.Value, attackingFaction: PlanetSideEmpire.Value,
@ -70,11 +68,12 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
): Unit = { ): Unit = {
val curr = System.currentTimeMillis() val curr = System.currentTimeMillis()
val hackStart = curr - completionTime val hackStart = curr - completionTime
val (victorFaction, opposingFaction, flagCarrier) = if (!isResecured) { val socketOpt = building.GetFlagSocket
val carrier = building.GetFlagSocket.flatMap(_.previousFlag).flatMap(_.Carrier) val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) {
(attackingFaction, defenderFaction, carrier) val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier)
(attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier)
} else { } else {
(defenderFaction, attackingFaction, None) (defenderFaction, attackingFaction, socketOpt.nonEmpty, None)
} }
val (contributionVictor, contributionOpposing, _) = { val (contributionVictor, contributionOpposing, _) = {
val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction } val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
@ -83,17 +82,6 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
} }
val contributionVictorSize = contributionVictor.size val contributionVictorSize = contributionVictor.size
if (contributionVictorSize > 0) { if (contributionVictorSize > 0) {
val contributionOpposingSize = contributionOpposing.size
//1) experience from killing opposingFaction across duration of hack
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
building.Position,
building.Definition.SOIRadius - 50f,
hackStart,
completionTime,
opposingFaction,
contributionVictor,
contributionOpposingSize
)
//setup for ... //setup for ...
val populationIndices = playerPopulationOverTime.indices val populationIndices = playerPopulationOverTime.indices
val allFactions = PlanetSideEmpire.values.filterNot { _ == PlanetSideEmpire.NEUTRAL }.toSeq val allFactions = PlanetSideEmpire.values.filterNot { _ == PlanetSideEmpire.NEUTRAL }.toSeq
@ -103,32 +91,43 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
}.toMap[PlanetSideEmpire.Value, Seq[Int]] }.toMap[PlanetSideEmpire.Value, Seq[Int]]
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction)) (individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
} }
val contributionOpposingSize = contributionOpposing.size
val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
building,
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
hackStart,
completionTime,
opposingFaction,
contributionOpposing
)
)
//1) experience from killing opposingFaction across duration of hack
//The kills that occurred in the facility's attached field tower's sphere of influence have been eliminated from consideration.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
killsByPlayersNotInTower,
contributionOpposingSize
)
//2) peak population modifier //2) peak population modifier
//Large facility battles should be well-rewarded.
val populationModifier = FacilityHackParticipation.populationProgressModifier( val populationModifier = FacilityHackParticipation.populationProgressModifier(
opposingPopulationByLayer, opposingPopulationByLayer,
{ pop => { pop =>
if (pop > 59) 0.5f if (pop > 75) 0.9f
else if (pop > 29) 0.4f else if (pop > 59) 0.6f
else if (pop > 25) 0.3f else if (pop > 29) 0.55f
else 0.25f else if (pop > 25) 0.5f
else 0.45f
}, },
3 4
) )
//3) competition bonus //3) competition multiplier
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize,
contributionOpposingSize,
steamrollPercentage = 1.25f,
steamrollBonus = 5L,
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 100L
)
//4) competition multiplier
val competitionMultiplier: Float = { val competitionMultiplier: Float = {
val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier( val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer, victorPopulationByLayer,
opposingPopulationByLayer, opposingPopulationByLayer,
healthyPercentage = 1.25f healthyPercentage = 1.5f
) )
//compensate for heat //compensate for heat
val regionHeatMapProgression = { val regionHeatMapProgression = {
@ -140,7 +139,11 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
((A-1, B-2, C-3), (D-1, E-2, F-3), (G-1, H-2, I-3)) ... (1->(A, D, G), 2->(B, E, H), 3->(C, F, I)) ((A-1, B-2, C-3), (D-1, E-2, F-3), (G-1, H-2, I-3)) ... (1->(A, D, G), 2->(B, E, H), 3->(C, F, I))
*/ */
val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]() val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]()
.addAll(hotSpotLayersOverTime.head.map { entry => (entry.DisplayLocation, Map.empty) }) .addAll(
hotSpotLayersOverTime.take(1).flatMap { entry =>
entry.map { f => (f.DisplayLocation, Map.empty[PlanetSideEmpire.Value, Seq[Long]]) }
}
)
//note: this pre-seeding of keys allows us to skip a getOrElse call in the foldLeft //note: this pre-seeding of keys allows us to skip a getOrElse call in the foldLeft
hotSpotLayersOverTime.foldLeft(finalMap) { (map, list) => hotSpotLayersOverTime.foldLeft(finalMap) { (map, list) =>
list.foreach { entry => list.foreach { entry =>
@ -153,48 +156,131 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
}.toMap }.toMap
finalMap //explicit for no good reason finalMap //explicit for no good reason
} }
val heatVictorMap = FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values val heatMapModifier = FacilityHackParticipation.heatMapComparison(
val heatAgainstMap = FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values,
val heatMapModifier = FacilityHackParticipation.heatMapComparison(heatVictorMap, heatAgainstMap) FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values
)
heatMapModifier * populationBalanceModifier heatMapModifier * populationBalanceModifier
} }
//5) hack time modifier //4) hack time modifier
val overallTimeMultiplier: Float = if (isResecured) { //Captured major facilities without a lattice link unit and resecured major facilities with a lattice link unit
math.max(0.5f + (hackTime - completionTime) / hackTime, 1f) // incur the full hack time if the module is not transported to a friendly facility
} else { //Captured major facilities with a lattice link unit and resecure major facilities without a lattice link uit
1f // will incur an abbreviated duration
val overallTimeMultiplier: Float = {
if (
building.Faction == PlanetSideEmpire.NEUTRAL ||
building.NtuLevel == 0 ||
building.Generator.map { _.Condition }.contains(PlanetSideGeneratorState.Destroyed)
) { //the facility ran out of nanites or power during the hack or became neutral
0f
} else if (hasFlag) {
if (completionTime >= hackTime) { //hack timed out without llu delivery
0.25f
} else if (isResecured) {
0.5f + (if (hackTime <= completionTime * 0.3f) {
completionTime.toFloat / hackTime.toFloat
} else if (hackTime >= completionTime * 0.6f) {
(hackTime - completionTime).toFloat / hackTime.toFloat
} else {
0f
})
} else {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
}
} else {
if (isResecured) {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
} else {
0.5f
}
}
} }
//calculate overall command experience points and the individual player multiplier //5. individual contribution factors - by time
val contributionPerPlayerByTime = playerContribution.collect {
case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 =>
(a, 0.45f)
case (a, (_, d, _)) if d >= 600000 =>
(a, 0.25f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f * (0.5f + (d.toFloat / 600000f)))
case (a, (_, _, _)) =>
(a, 0.15f)
}
//6. competition bonus
//This value will probably suck, and that's fine.
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize,
contributionOpposingSize,
steamrollPercentage = 1.25f,
steamrollBonus = 5L,
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 15L
)
//7. calculate overall command experience points
val finalCep: Long = math.ceil( val finalCep: Long = math.ceil(
math.max(1L, baseExperienceFromFacilityCapture + competitionBonus) * math.max(0L, baseExperienceFromFacilityCapture) *
populationModifier * populationModifier *
competitionMultiplier * competitionMultiplier *
overallTimeMultiplier * overallTimeMultiplier *
Config.app.game.cepRate Config.app.game.experience.cep.rate + competitionBonus
).toLong ).toLong
val contributionPerPlayerByTime = playerContribution.collect { //8. reward participants
case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 => //Classically, only players in the SOI are rewarded, and the llu runner too
(a, 1f) val hackerId = hacker.CharId
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, d.toFloat / 6000000f)
case (a, (_, d, t)) =>
(a, math.max(0, d - completionTime + t).toFloat / 6000000f)
}
//reward participant(s)
// classically, only players in the SOI are rewarded
val events = building.Zone.AvatarEvents val events = building.Zone.AvatarEvents
building.PlayersInSOI val playersInSoi = building.PlayersInSOI
.filter { player => if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
player.Faction == victorFaction && player.CharId != hacker.CharId && !flagCarrier.contains(player) events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
} }
playersInSoi
.filter { player => player.Faction == victorFaction && player.CharId != hackerId }
.foreach { player => .foreach { player =>
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(player.CharId, 1f) val charId = player.CharId
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, (finalCep * contributionMultiplier).toLong)) val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
val outputValue = (finalCep * contributionMultiplier).toLong
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, outputValue))
} }
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
flagCarrier.collect { flagCarrier.collect {
player => events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(player.CharId, (finalCep * 0.5f).toLong)) case player if !isResecured =>
val charId: Long = player.CharId
val finalModifiedCep: Long = math.min(
(hackTime - completionTime) / 1000L,
(finalCep * Config.app.game.experience.cep.lluCarrierModifier).toLong
)
ToDatabase.reportFacilityCapture(
charId,
building.Zone.Number,
building.GUID.guid,
finalModifiedCep,
expType="llu"
)
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(charId, finalModifiedCep))
} }
} }
} }
private def eliminateClosestTowerFromParticipating(
building: Building,
list: Iterable[(UniquePlayer, Float, Seq[Kill])]
): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
val buildingPosition = building.Position.xy
building
.Zone
.Buildings
.values
.filter { building => building.BuildingType == StructureType.Tower }
.minByOption { tower => Vector3.DistanceSquared(buildingPosition, tower.Position.xy) }
.map { tower =>
val towerPosition = tower.Position.xy
val towerRadius = math.pow(tower.Definition.SOIRadius.toDouble * 0.7d, 2d).toFloat
list
.map { case (p, f, kills) =>
val filteredKills = kills.filter { kill => Vector3.DistanceSquared(kill.victim.Position.xy, towerPosition) <= towerRadius }
(p, f, filteredKills)
}
.filter { case (_, _, kills) => kills.nonEmpty }
}
.getOrElse(list)
}
} }

View file

@ -4,7 +4,7 @@ package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation { final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation {
@ -25,8 +25,6 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
completionTime: Long, completionTime: Long,
isResecured: Boolean isResecured: Boolean
): Unit = { ): Unit = {
val curr = System.currentTimeMillis()
val hackStart = curr - completionTime
val (victorFaction, opposingFaction) = if (!isResecured) { val (victorFaction, opposingFaction) = if (!isResecured) {
(attackingFaction, defenderFaction) (attackingFaction, defenderFaction)
} else { } else {
@ -40,18 +38,10 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
} }
val contributionVictorSize = contributionVictor.size val contributionVictorSize = contributionVictor.size
if (contributionVictorSize > 0) { if (contributionVictorSize > 0) {
val contributionOpposingSize = contributionOpposing.size
//1) experience from killing opposingFaction across duration of hack
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
building.Position,
building.Definition.SOIRadius.toFloat,
hackStart,
completionTime,
opposingFaction,
contributionVictor,
contributionOpposingSize
)
//setup for ... //setup for ...
import scala.concurrent.duration._
val curr = System.currentTimeMillis()
val contributionOpposingSize = contributionOpposing.size
val populationIndices = playerPopulationOverTime.indices val populationIndices = playerPopulationOverTime.indices
val allFactions = PlanetSideEmpire.values.filterNot { val allFactions = PlanetSideEmpire.values.filterNot {
_ == PlanetSideEmpire.NEUTRAL _ == PlanetSideEmpire.NEUTRAL
@ -62,18 +52,74 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
}.toMap[PlanetSideEmpire.Value, Seq[Int]] }.toMap[PlanetSideEmpire.Value, Seq[Int]]
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction)) (individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
} }
val soiPlayers = building.PlayersInSOI
//1) experience from killing opposingFaction
//Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
curr - 5.minutes.toMillis,
curr,
opposingFaction,
contributionVictor
),
contributionOpposingSize
)
//2) peak population modifier //2) peak population modifier
//Towers should not be regarded as major battles.
//As the population rises, the rewards decrease (dramatically).
val populationModifier = FacilityHackParticipation.populationProgressModifier( val populationModifier = FacilityHackParticipation.populationProgressModifier(
opposingPopulationByLayer, victorPopulationByLayer,
{ pop => { pop =>
if (pop > 59) 0.75f if (pop > 80) 0f
else if (pop > 29) 0.675f else if (pop > 39) (80 - pop).toFloat * 0.01f
else if (pop > 25) 0.55f else if (pop > 25) 0.5f
else 0.5f else if (pop > 19) 0.55f
else if (pop > 9) 0.6f
else if (pop > 5) 0.75f
else 1f
}, },
2 2
) )
//3) competition bonus //3) competition multiplier
val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.25f
)
//4a. individual contribution factors - by time
//Once again, an arbitrary five minute period.
val contributionPerPlayerByTime = playerContribution.collect {
case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 =>
(a, 0.45f)
case (a, (_, d, _)) if d >= 300000 =>
(a, 0.25f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f * (0.5f + (d.toFloat / 300000f)))
case (a, (_, _, _)) =>
(a, 0.15f)
}
//4b. individual contribution factors - by distance to goal (secondary_capture)
//Because the hack duration of towers is instantaneous, distance from terminal is a more important factor
val contributionPerPlayerByDistanceFromGoal = {
var minDistance: Float = Float.PositiveInfinity
val location = building
.CaptureTerminal
.map { terminal => terminal.Position }
.getOrElse { hacker.Position }
soiPlayers
.map { p =>
val distance = Vector3.Distance(p.Position, location)
minDistance = math.min(minDistance, distance)
(p.CharId, distance)
}
.map { case (id, distance) =>
(id, math.max(0.15f, minDistance / distance))
}
}.toMap[Long, Float]
//5) token competition bonus
//This value will probably suck, and that's fine.
val competitionBonus: Long = FacilityHackParticipation.competitionBonus( val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize, contributionVictorSize,
contributionOpposingSize, contributionOpposingSize,
@ -82,37 +128,29 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
overwhelmingOddsPercentage = 0.5f, overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 30L overwhelmingOddsBonus = 30L
) )
//4) competition multiplier //6. calculate overall command experience points
val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.25f
)
//calculate overall command experience points and the individual player multiplier
val finalCep: Long = math.ceil( val finalCep: Long = math.ceil(
math.max(1L, baseExperienceFromFacilityCapture + competitionBonus) * baseExperienceFromFacilityCapture *
populationModifier * populationModifier *
competitionMultiplier * competitionMultiplier *
Config.app.game.cepRate Config.app.game.experience.cep.rate + competitionBonus
).toLong ).toLong
val contributionPerPlayerByTime = playerContribution.collect { //7. reward participants
case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 => //Classically, only players in the SOI are rewarded
(a, 0.5f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f * (1f + (d.toFloat / 3000000f)))
case (a, (_, d, t)) =>
(a, 0.25f * (1f + (math.max(0, d - completionTime + t).toFloat / 3000000f)))
}
//reward participant(s)
// classically, only players in the SOI are rewarded
val events = building.Zone.AvatarEvents val events = building.Zone.AvatarEvents
building.PlayersInSOI soiPlayers
.filter { player => .filter { player =>
player.Faction == victorFaction && player.CharId != hacker.CharId player.Faction == victorFaction && player.CharId != hacker.CharId
} }
.foreach { player => .foreach { player =>
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(player.CharId, 1f) val charId = player.CharId
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, (finalCep * contributionMultiplier).toLong)) val contributionTimeMultiplier = contributionPerPlayerByTime.getOrElse(charId, 0.5f)
val contributionDistanceMultiplier = contributionPerPlayerByDistanceFromGoal.getOrElse(charId, 0.5f)
val outputValue = (finalCep * contributionTimeMultiplier * contributionDistanceMultiplier).toLong
events ! AvatarServiceMessage(
player.Name,
AvatarAction.AwardCep(0, outputValue)
)
} }
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep)) events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
} }

View file

@ -101,7 +101,7 @@ class FacilityTurretControl(turret: FacilityTurret)
turret.ControlledWeapon(wepNumber = 1).foreach { turret.ControlledWeapon(wepNumber = 1).foreach {
case weapon: Tool => case weapon: Tool =>
// recharge when last shot fired 3s delay, +1, 200ms interval // recharge when last shot fired 3s delay, +1, 200ms interval
if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) { if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
weapon.Magazine += 1 weapon.Magazine += 1
val seat = turret.Seat(0).get val seat = turret.Seat(0).get
seat.occupant match { seat.occupant match {

View file

@ -187,8 +187,10 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
val chargeToDeposit = if (min == 0) { val chargeToDeposit = if (min == 0) {
transferTarget match { transferTarget match {
case Some(silo: ResourceSilo) => case Some(silo: ResourceSilo) =>
// Silos would charge from 0-100% in roughly 105s on live (~20%-100% https://youtu.be/veOWToR2nSk?t=1402) scala.math.min(
scala.math.min(scala.math.min(silo.MaxNtuCapacitor / 105, chargeable.NtuCapacitor), max) scala.math.min(silo.MaxNtuCapacitor / silo.Definition.ChargeTime.toMillis.toFloat, chargeable.NtuCapacitor),
max
)
case _ => case _ =>
0 0
} }

View file

@ -893,7 +893,7 @@ object VehicleControl {
/** /**
* Determine if a given activity entry would invalidate the act of charging vehicle shields this tick. * Determine if a given activity entry would invalidate the act of charging vehicle shields this tick.
* @param now the current time (in nanoseconds) * @param now the current time (in milliseconds)
* @param act a `VitalsActivity` entry to test * @param act a `VitalsActivity` entry to test
* @return `true`, if the shield charge would be blocked; * @return `true`, if the shield charge would be blocked;
* `false`, otherwise * `false`, otherwise

View file

@ -10,6 +10,7 @@ import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, Su
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType} import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
import net.psforever.util.Config
import scala.collection.mutable import scala.collection.mutable
@ -327,7 +328,7 @@ trait InGameHistory {
} }
def GetContribution(): Option[List[InGameActivity]] = { def GetContribution(): Option[List[InGameActivity]] = {
Option(GetContributionDuringPeriod(History, duration = 600000L)) Option(GetContributionDuringPeriod(History, duration = Config.app.game.experience.longContributionTime))
} }
def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = { def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {

View file

@ -78,23 +78,23 @@ object KillAssists {
lastDamage: Option[DamageResult], lastDamage: Option[DamageResult],
history: Iterable[InGameActivity], history: Iterable[InGameActivity],
): Seq[(PlayerSource, KDAStat)] = { ): Seq[(PlayerSource, KDAStat)] = {
val shortHistory = limitHistoryToThisLife(history.toList) val truncatedHistory = limitHistoryToThisLife(history.toList)
determineKiller(lastDamage, shortHistory) match { determineKiller(lastDamage, truncatedHistory) match {
case Some((result, killer: PlayerSource)) => case Some((result, killer: PlayerSource)) =>
val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer)) val assists = collectKillAssistsForPlayer(victim, truncatedHistory, Some(killer))
val fullBep = calculateExperience(killer, victim, shortHistory) val fullBep = calculateExperience(killer, victim, truncatedHistory)
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map { val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong)) case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq }.toSeq
(victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad (victim, Death(hitSquad.map { _._1 }, truncatedHistory.last.time - truncatedHistory.head.time, fullBep)) +: hitSquad
case _ => case _ =>
val assists = collectKillAssistsForPlayer(victim, shortHistory, None) val assists = collectKillAssistsForPlayer(victim, truncatedHistory, None)
val fullBep = Support.baseExperience(victim, shortHistory) val fullBep = Support.baseExperience(victim, truncatedHistory)
val hitSquad = assists.map { val hitSquad = assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong)) case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq }.toSeq
(victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad (victim, Death(hitSquad.map { _._1 }, truncatedHistory.last.time - truncatedHistory.head.time, fullBep)) +: hitSquad
} }
} }

View file

@ -10,6 +10,7 @@ import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess} import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.Config
import scala.collection.mutable import scala.collection.mutable
@ -49,7 +50,7 @@ object KillContributions {
multivehicle_rearm_terminal, multivehicle_rearm_terminal,
lodestar_repair_terminal lodestar_repair_terminal
).collect { _.ObjectId } ).collect { _.ObjectId }
} //TODO currently includes things that are not typical items but are used for expressing contribution implements } //TODO currently includes things that are not typical equipment but things that express contribution
/** cached for empty collection returns; please do not add anything to it */ /** cached for empty collection returns; please do not add anything to it */
private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats] private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats]
@ -119,21 +120,20 @@ object KillContributions {
): Iterable[(Long, ContributionStatsOutput)] = { ): Iterable[(Long, ContributionStatsOutput)] = {
val faction = target.Faction val faction = target.Faction
/* /*
divide into applicable time periods - long for 10 minutes and short 5 minutes; divide into applicable time periods;
these two periods represent passes over the in-game history to evaluate statistic modification events; these two periods represent passes over the in-game history to evaluate statistic modification events;
the short time period should stand on its own, but should also be represented in the long time period; the short time period should stand on its own, but should also be represented in the long time period;
more players should be rewarded if one qualifies for the longer time period's evaluation more players should be rewarded if one qualifies for the longer time period's evaluation
*/ */
//divide by applicable time periods (long=10minutes, short=5minutes) val (contributions, (longHistory, shortHistory)) = {
val (contributions, (shortHistory, longHistory)) = {
val killTime = kill.time.toDate.getTime val killTime = kill.time.toDate.getTime
val shortPeriod = killTime - 300000L val shortPeriod = killTime - Config.app.game.experience.shortContributionTime
val (contrib, onlyHistory) = history.partition { _.isInstanceOf[Contribution] } val (contrib, onlyHistory) = history.partition { _.isInstanceOf[Contribution] }
( (
contrib contrib
.collect { case Contribution(unique, entries) => (unique, entries) } .collect { case Contribution(unique, entries) => (unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]], .toMap[SourceUniqueness, List[InGameActivity]],
limitHistoryToThisLife(onlyHistory.toList, killTime).partition { _.time > shortPeriod } limitHistoryToThisLife(onlyHistory.toList, killTime).partition { _.time < shortPeriod }
) )
} }
//events that are older than 5 minutes are enough to prove one has been alive that long //events that are older than 5 minutes are enough to prove one has been alive that long
@ -195,7 +195,7 @@ object KillContributions {
* @return the potentially truncated history * @return the potentially truncated history
*/ */
private def limitHistoryToThisLife(history: List[InGameActivity], eventTime: Long): List[InGameActivity] = { private def limitHistoryToThisLife(history: List[InGameActivity], eventTime: Long): List[InGameActivity] = {
limitHistoryToThisLife(history, eventTime, eventTime - 600000L) limitHistoryToThisLife(history, eventTime, eventTime - Config.app.game.experience.longContributionTime)
} }
/** /**
@ -262,9 +262,9 @@ object KillContributions {
excludedTargets: mutable.ListBuffer[SourceUniqueness] excludedTargets: mutable.ListBuffer[SourceUniqueness]
): mutable.LongMap[ContributionStats] = { ): mutable.LongMap[ContributionStats] = {
contributeWithRevivalActivity(history, existingParticipants) contributeWithRevivalActivity(history, existingParticipants)
contributeWithTerminalActivity(faction, history, contributions, excludedTargets, existingParticipants) contributeWithTerminalActivity(history, faction, contributions, excludedTargets, existingParticipants)
contributeWithVehicleTransportActivity(history, faction, contributions, excludedTargets, existingParticipants) contributeWithVehicleTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
contributeWithVehicleCargoTransportActivity(history, faction, contributions, excludedTargets, existingParticipants) contributeWithVehicleCargoTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants) contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants)
existingParticipants.remove(0) existingParticipants.remove(0)
existingParticipants existingParticipants
@ -314,13 +314,17 @@ object KillContributions {
excludedTargets.addOne(owner) excludedTargets.addOne(owner)
excludedTargets.addOne(attacker.unique) excludedTargets.addOne(attacker.unique)
val time = kill.time.toDate.getTime val time = kill.time.toDate.getTime
val weaponStat = Support.calculateSupportExperience(
event = "mounted-kill",
WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 1f)
)
combineStatsInto( combineStatsInto(
out, out,
( (
owner.charId, owner.charId,
ContributionStats( ContributionStats(
PlayerSource(owner, mount.Position), PlayerSource(owner, mount.Position),
Seq(WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 10f)), Seq(weaponStat),
1, 1,
1, 1,
1, 1,
@ -332,12 +336,12 @@ object KillContributions {
} }
combineStatsInto( combineStatsInto(
out, out,
extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets) extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets, eventOutputType="support-repair")
) )
case Some((mount: TurretSource, _: PlayerSource)) if !excludedTargets.contains(mount.unique) => case Some((mount: TurretSource, _: PlayerSource)) if !excludedTargets.contains(mount.unique) =>
combineStatsInto( combineStatsInto(
out, out,
extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets) extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets, eventOutputType="support-repair-turret")
) )
} }
} }
@ -345,6 +349,7 @@ object KillContributions {
/** /**
* Gather and reward specific in-game equipment use activity.<br> * Gather and reward specific in-game equipment use activity.<br>
* na * na
* @param kill the in-game event that maintains information about the other player's death
* @param history chronology of activity the game considers noteworthy * @param history chronology of activity the game considers noteworthy
* @param faction empire to target * @param faction empire to target
* @param contributions mapping between external entities * @param contributions mapping between external entities
@ -356,6 +361,7 @@ object KillContributions {
* @see `extractContributionsForMachineByTarget` * @see `extractContributionsForMachineByTarget`
*/ */
private def contributeWithVehicleTransportActivity( private def contributeWithVehicleTransportActivity(
kill: Kill,
history: List[InGameActivity], history: List[InGameActivity],
faction: PlanetSideEmpire.Value, faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]], contributions: Map[SourceUniqueness, List[InGameActivity]],
@ -385,11 +391,19 @@ object KillContributions {
} }
} || { } || {
val sameZone = in.zoneNumber == out.zoneNumber val sameZone = in.zoneNumber == out.zoneNumber
val distanceMoved = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy) val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
val distanceMoved = {
val killLocation = kill.info.adversarial
.collect { adversarial => adversarial.attacker.Position.xy }
.getOrElse(Vector3.Zero)
Vector3.DistanceSquared(killLocation, out.player.Position.xy)
}
val timeSpent = out.time - in.time val timeSpent = out.time - in.time
timeSpent >= 210000 /* 3:30 */ || distanceMoved < 5625f /* 75m */ &&
(sameZone && (distanceMoved > 160000f || distanceMoved > 10000f && timeSpent >= 60000)) | (timeSpent >= 210000L /* 3:30 */ ||
(!sameZone && (distanceMoved > 10000f || timeSpent >= 120000)) (sameZone && (distanceTransported > 160000f /* 400m */ ||
distanceTransported > 10000f /* 100m */ && timeSpent >= 60000L /* 1:00m */)) ||
(!sameZone && (distanceTransported > 10000f /* 100m */ || timeSpent >= 120000L /* 2:00 */ )))
}) => }) =>
out out
} }
@ -398,18 +412,23 @@ object KillContributions {
.groupBy { _.vehicle } .groupBy { _.vehicle }
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty => .collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
val promotedOwner = PlayerSource(mount.owner.get, mount.Position) val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
val equipmentUseContext = mount.Definition match { val (equipmentUseContext, equipmentUseEvent) = mount.Definition match {
case v @ GlobalDefinitions.router => case v @ GlobalDefinitions.router =>
RouterKillAssist(v.ObjectId) (RouterKillAssist(v.ObjectId), "router")
case v => case v =>
HotDropKillAssist(v.ObjectId, 0) (HotDropKillAssist(v.ObjectId, 0), "hotdrop")
} }
val statContext = Seq(WeaponStats(equipmentUseContext, 0, dismountsFromVehicle.size, dismountsFromVehicle.maxBy(_.time).time, 15f)) val size = dismountsFromVehicle.size
val time = dismountsFromVehicle.maxBy(_.time).time
val weaponStat = Support.calculateSupportExperience(
equipmentUseEvent,
WeaponStats(equipmentUseContext, size, size, time, 1f)
)
combineStatsInto( combineStatsInto(
out, out,
( (
promotedOwner.CharId, promotedOwner.CharId,
ContributionStats(promotedOwner, statContext, 1, 1, 1, statContext.head.time) ContributionStats(promotedOwner, Seq(weaponStat), size, size, size, time)
) )
) )
contributions.get(mount.unique).collect { contributions.get(mount.unique).collect {
@ -417,13 +436,13 @@ object KillContributions {
val mountHistory = dismountsFromVehicle val mountHistory = dismountsFromVehicle
.flatMap { event => .flatMap { event =>
val eventTime = event.time val eventTime = event.time
val startTime = event.pairedEvent.get.time - 600000L val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime) limitHistoryToThisLife(list, eventTime, startTime)
} }
.distinctBy(_.time) .distinctBy(_.time)
combineStatsInto( combineStatsInto(
out, out,
extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets) extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
) )
} }
} }
@ -432,6 +451,7 @@ object KillContributions {
/** /**
* Gather and reward specific in-game equipment use activity.<br> * Gather and reward specific in-game equipment use activity.<br>
* na * na
* @param kill the in-game event that maintains information about the other player's death
* @param faction empire to target * @param faction empire to target
* @param contributions mapping between external entities * @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity * the target has interacted with in the form of in-game activity
@ -442,6 +462,7 @@ object KillContributions {
* @see `extractContributionsForMachineByTarget` * @see `extractContributionsForMachineByTarget`
*/ */
private def contributeWithVehicleCargoTransportActivity( private def contributeWithVehicleCargoTransportActivity(
kill: Kill,
history: List[InGameActivity], history: List[InGameActivity],
faction: PlanetSideEmpire.Value, faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]], contributions: Map[SourceUniqueness, List[InGameActivity]],
@ -463,9 +484,16 @@ object KillContributions {
if in.vehicle.unique == out.vehicle.unique && if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.cargo.Faction && out.vehicle.Faction == out.cargo.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || { (in.vehicle.Definition == GlobalDefinitions.router || {
val distanceMoved = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy) val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
val distanceMoved = {
val killLocation = kill.info.adversarial
.collect { adversarial => adversarial.attacker.Position.xy }
.getOrElse(Vector3.Zero)
Vector3.DistanceSquared(killLocation, out.cargo.Position.xy)
}
val timeSpent = out.time - in.time val timeSpent = out.time - in.time
timeSpent >= 210000 /* 3:30 */ || distanceMoved > 640000f distanceMoved < 5625f /* 75m */ &&
(timeSpent >= 210000 /* 3:30 */ || distanceTransported > 360000f /* 600m */)
}) => }) =>
out out
} }
@ -478,7 +506,13 @@ object KillContributions {
dismountsFromVehicle dismountsFromVehicle
.groupBy(_.vehicle) .groupBy(_.vehicle)
.map { case (vehicle, events) => .map { case (vehicle, events) =>
(vehicle, vehicle.owner, Seq(WeaponStats(HotDropKillAssist(vehicle.Definition.ObjectId, mountId), 0, events.size, events.maxBy(_.time).time, 15f))) val size = events.size
val time = events.maxBy(_.time).time
val weaponStat = Support.calculateSupportExperience(
event = "hotdrop",
WeaponStats(HotDropKillAssist(vehicle.Definition.ObjectId, mountId), size, size, time, 1f)
)
(vehicle, vehicle.owner, Seq(weaponStat))
} }
.collect { case (vehicle, Some(owner), statContext) => .collect { case (vehicle, Some(owner), statContext) =>
combineStatsInto( combineStatsInto(
@ -493,13 +527,13 @@ object KillContributions {
val mountHistory = dismountsFromVehicle val mountHistory = dismountsFromVehicle
.flatMap { event => .flatMap { event =>
val eventTime = event.time val eventTime = event.time
val startTime = event.pairedEvent.get.time - 600000L val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime) limitHistoryToThisLife(list, eventTime, startTime)
} }
.distinctBy(_.time) .distinctBy(_.time)
combineStatsInto( combineStatsInto(
out, out,
extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets) extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
) )
} }
contributions.get(vehicle.unique).collect { contributions.get(vehicle.unique).collect {
@ -507,13 +541,13 @@ object KillContributions {
val carrierHistory = dismountsFromVehicle val carrierHistory = dismountsFromVehicle
.flatMap { event => .flatMap { event =>
val eventTime = event.time val eventTime = event.time
val startTime = event.pairedEvent.get.time - 600000L val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime) limitHistoryToThisLife(list, eventTime, startTime)
} }
.distinctBy(_.time) .distinctBy(_.time)
combineStatsInto( combineStatsInto(
out, out,
extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets) extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets, eventOutputType="support-repair")
) )
} }
} }
@ -542,104 +576,120 @@ object KillContributions {
* @see `TerminalUsedActivity` * @see `TerminalUsedActivity`
*/ */
private def contributeWithTerminalActivity( private def contributeWithTerminalActivity(
faction: PlanetSideEmpire.Value,
history: List[InGameActivity], history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]], contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness], excludedTargets: mutable.ListBuffer[SourceUniqueness],
out: mutable.LongMap[ContributionStats] out: mutable.LongMap[ContributionStats]
): Unit = { ): Unit = {
val data = history history
.collect { .collect {
case h: HealFromTerminal => (h.term, (h, h.term.hacked)) case h: HealFromTerminal => (h.term, h)
case r: RepairFromTerminal => (r.term, (r, r.term.hacked)) case r: RepairFromTerminal => (r.term, r)
case t: TerminalUsedActivity => (t.terminal, (t, t.terminal.hacked)) case t: TerminalUsedActivity => (t.terminal, t)
} }
.groupBy(_._1.unique) .groupBy(_._1.unique)
.map { case (_, list) => (list.head._1, list.map { _._2 }) } .map {
data.flatMap { case (_, events1) =>
case (terminal, events) => val (termThings1, _) = events1.unzip
val (activity, hackState) = events.unzip val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, termThings1.head.Definition.ObjectId)
val terminalFaction = terminal.Faction if (termThings1.exists(t => t.Faction != faction && t.hacked.nonEmpty)) {
if (terminalFaction != faction && hackState.exists { _.nonEmpty }) { /*
/* if the terminal has been hacked,
if the terminal has been hacked, and the original terminal does not align with our own faction,
and the original terminal does not align with our own faction, then the support must be reported as a hack;
then the support must be reported as a hack; if we are the same faction as the terminal, then the hacked condition is irrelevant
if we are the same faction as the terminal, then the hacked condition is irrelevant */
*/ events1
val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, terminal.Definition.ObjectId) .collect { case out @ (t, _) if t.hacked.nonEmpty => out }
hackState .groupBy { case (t, _) => t.hacked.get.player.unique }
.groupBy(_.get.player) .foreach { case (_, events2) =>
.collect { val (termThings2, events3) = events2.unzip
case (player, _) if player.Faction == faction => //only reward allied hacking val hacker = termThings2.head.hacked.get.player
val time = activity.maxBy(_.time).time val size = events3.size
val time = events3.maxBy(_.time).time
val weaponStats = Support.calculateSupportExperience(
event = "hack",
WeaponStats(hackContext, size, size, time, 1f)
)
combineStatsInto( combineStatsInto(
out, out,
( (
player.CharId, hacker.CharId,
ContributionStats( ContributionStats(
player, hacker,
Seq(WeaponStats(hackContext, 0, 1, time, 10f)), Seq(weaponStats),
0, size,
0, size,
1, size,
time time
) )
) )
) )
}
activity
} else if (terminalFaction == faction) {
val eventTime = activity.maxBy(_.time).time
val startTime = activity.minBy(_.time).time - 600000L
val (equipmentUseContext, ownerOpt) = terminal.installation match {
case v: VehicleSource =>
terminal.Definition match {
case GlobalDefinitions.order_terminala =>
combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
(AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner)
case GlobalDefinitions.order_terminalb =>
combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
(AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner)
case GlobalDefinitions.lodestar_repair_terminal =>
combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
(RepairKillAssist(terminal.Definition.ObjectId, v.Definition.ObjectId), v.owner)
case GlobalDefinitions.bfr_rearm_terminal =>
combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
(LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner)
case GlobalDefinitions.multivehicle_rearm_terminal =>
combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
(LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner)
case _ =>
(NoUse(), None)
} }
case _: BuildingSource => } else if (termThings1.exists(_.Faction == faction)) {
combineStatsInto(out, extractContributionsForMachineByTarget(terminal, faction, eventTime, startTime, contributions, excludedTargets)) //faction-aligned terminal
(NoUse(), None) //general terminal use val (_, events2) = events1.unzip
case _ => val eventTime = events2.maxBy(_.time).time
(NoUse(), None) val startTime = events2.minBy(_.time).time - Config.app.game.experience.longContributionTime
} val termThingsHead = termThings1.head
ownerOpt.collect { owner => val (equipmentUseContext, equipmentUseEvent, installationEvent, target) = termThingsHead.installation match {
val time = activity.maxBy(_.time).time case v: VehicleSource =>
combineStatsInto( termThingsHead.Definition match {
out, case GlobalDefinitions.order_terminala =>
( (AmsResupplyKillAssist(GlobalDefinitions.order_terminala.ObjectId), "ams-resupply", "support-repair", Some(v))
owner.charId, case GlobalDefinitions.order_terminalb =>
ContributionStats( (AmsResupplyKillAssist(GlobalDefinitions.order_terminalb.ObjectId), "ams-resupply", "support-repair", Some(v))
PlayerSource(owner, terminal.installation.Position), case GlobalDefinitions.lodestar_repair_terminal =>
Seq(WeaponStats(equipmentUseContext, 0, 1, time, 10f)), (RepairKillAssist(GlobalDefinitions.lodestar_repair_terminal.ObjectId, v.Definition.ObjectId), "lodestar-repair", "support-repair", Some(v))
0, case GlobalDefinitions.bfr_rearm_terminal =>
0, (LodestarRearmKillAssist(GlobalDefinitions.bfr_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v))
1, case GlobalDefinitions.multivehicle_rearm_terminal =>
time (LodestarRearmKillAssist(GlobalDefinitions.multivehicle_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v))
case _ =>
(NoUse(), "", "", None)
}
case _: BuildingSource =>
(NoUse(), "", "support-repair-terminal", Some(termThingsHead))
case _ =>
(NoUse(), "", "", None)
}
target.map { src =>
combineStatsInto(
out,
extractContributionsForMachineByTarget(src, faction, eventTime, startTime, contributions, excludedTargets, installationEvent)
)
}
events1
.map { case (a, b) => (a.installation, b) }
.collect { case (installation: VehicleSource, evt) if installation.owner.nonEmpty => (installation, evt) }
.groupBy(_._1.owner.get)
.collect { case (owner, list) =>
val (installations, events2) = list.unzip
val size = events2.size
val time = events2.maxBy(_.time).time
val weaponStats = Support.calculateSupportExperience(
equipmentUseEvent,
WeaponStats(equipmentUseContext, size, size, time, 1f)
) )
)) combineStatsInto(
out,
(
owner.charId,
ContributionStats(
PlayerSource(owner, installations.head.Position),
Seq(weaponStats),
size,
size,
size,
time
)
)
)
}
} }
activity None
} else { }
Nil
}
}
} }
/** /**
@ -671,7 +721,12 @@ object KillContributions {
id, id,
ContributionStats( ContributionStats(
user, user,
Seq(WeaponStats(ReviveKillAssist(objectId), 100, 1, time, math.log(eventSize + 1).toFloat * 15f)), Seq({
Support.calculateSupportExperience(
event = "revival",
WeaponStats(ReviveKillAssist(objectId), 1, eventSize, time, 1f)
)
}),
eventSize, eventSize,
eventSize, eventSize,
eventSize, eventSize,
@ -700,10 +755,11 @@ object KillContributions {
faction: PlanetSideEmpire.Value, faction: PlanetSideEmpire.Value,
time: Long, time: Long,
contributions: Map[SourceUniqueness, List[InGameActivity]], contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness] excludedTargets: mutable.ListBuffer[SourceUniqueness],
eventOutputType: String
): mutable.LongMap[ContributionStats] = { ): mutable.LongMap[ContributionStats] = {
val start: Long = time - 600000L val start: Long = time - Config.app.game.experience.longContributionTime
extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets) extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets, eventOutputType)
} }
/** /**
@ -726,11 +782,12 @@ object KillContributions {
eventTime: Long, eventTime: Long,
startTime: Long, startTime: Long,
contributions: Map[SourceUniqueness, List[InGameActivity]], contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness] excludedTargets: mutable.ListBuffer[SourceUniqueness],
eventOutputType: String
): mutable.LongMap[ContributionStats] = { ): mutable.LongMap[ContributionStats] = {
val unique = target.unique val unique = target.unique
val history = limitHistoryToThisLife(contributions.getOrElse(unique, List()), eventTime, startTime) val history = limitHistoryToThisLife(contributions.getOrElse(unique, List()), eventTime, startTime)
extractContributionsForMachineByTarget(target, faction, history, contributions, excludedTargets) extractContributionsForMachineByTarget(target, faction, history, contributions, excludedTargets, eventOutputType)
} }
/** /**
@ -753,12 +810,13 @@ object KillContributions {
faction: PlanetSideEmpire.Value, faction: PlanetSideEmpire.Value,
history: List[InGameActivity], history: List[InGameActivity],
contributions: Map[SourceUniqueness, List[InGameActivity]], contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness] excludedTargets: mutable.ListBuffer[SourceUniqueness],
eventOutputType: String
): mutable.LongMap[ContributionStats] = { ): mutable.LongMap[ContributionStats] = {
val unique = target.unique val unique = target.unique
if (!excludedTargets.contains(unique) && history.nonEmpty) { if (!excludedTargets.contains(unique) && history.nonEmpty) {
excludedTargets.addOne(unique) excludedTargets.addOne(unique)
val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, excludedTargets) val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, eventOutputType, excludedTargets)
process.submit(history) process.submit(history)
cullContributorImplements(process.output()) cullContributorImplements(process.output())
} else { } else {

View file

@ -4,6 +4,7 @@ package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.{InGameActivity, ReconstructionActivity, RepairFromExoSuitChange, SpawningActivity} import net.psforever.objects.vital.{InGameActivity, ReconstructionActivity, RepairFromExoSuitChange, SpawningActivity}
import net.psforever.types.{ExoSuitType, PlanetSideEmpire} import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
import net.psforever.util.Config
import scala.collection.mutable import scala.collection.mutable
@ -11,6 +12,8 @@ import scala.collection.mutable
* Functions to assist experience calculation and history manipulation and analysis. * Functions to assist experience calculation and history manipulation and analysis.
*/ */
object Support { object Support {
private val sep = Config.app.game.experience.sep
/** /**
* Calculate a base experience value to consider additional reasons for points. * Calculate a base experience value to consider additional reasons for points.
* @param victim player to which a final interaction has reduced health to zero * @param victim player to which a final interaction has reduced health to zero
@ -27,18 +30,19 @@ object Support {
case _ => 0L case _ => 0L
} }
val base = if (Support.wasEverAMax(victim, history)) { val base = if (Support.wasEverAMax(victim, history)) {
250L Config.app.game.experience.bep.base.asMax
} else if (victim.Seated || victim.progress.kills.nonEmpty) { } else if (victim.progress.kills.nonEmpty) {
100L Config.app.game.experience.bep.base.withKills
} else if (victim.Seated) {
Config.app.game.experience.bep.base.asMounted
} else if (lifespan > 15000L) { } else if (lifespan > 15000L) {
50L Config.app.game.experience.bep.base.mature
} else { } else {
1L 1L
} }
if (base > 1) { if (base > 1) {
//black ops modifier //black ops modifier
//TODO x10 base * Config.app.game.experience.bep.base.bopsMultiplier
base
} else { } else {
base base
} }
@ -187,4 +191,49 @@ object Support {
case _ => false case _ => false
} }
} }
/**
* Take a weapon statistics entry and calculate the support experience value resulting from this support event.
* The complete formula is:<br><br>
* `base + shots-multplier * ln(shots^exp + 2) + amount-multiplier * amount`<br><br>
* ... where the middle field can be truncated into:<br><br>
* `shots-multplier * shots`<br><br>
* ... without the natural logarithm exponent defined.
* Limits can be applied to the number of shots and/or to the amount,
* which will either zero the calculations or cap the results.
* @param event identification for the event calculation parameters
* @param weaponStat base weapon stat entry to be modified
* @param canNotFindEventDefaultValue custom default value
* @return weapon stat entry with a modified for the experience
*/
private[exp] def calculateSupportExperience(
event: String,
weaponStat: WeaponStats,
canNotFindEventDefaultValue: Option[Float] = None
): WeaponStats = {
val rewards: Float = sep.events
.find(evt => event.equals(evt.name))
.map { event =>
val shots = weaponStat.shots
val shotsMultiplier = event.shotsMultiplier
if (shotsMultiplier > 0f && shots < event.shotsCutoff) {
val modifiedShotsReward: Float = {
val partialShots = math.min(event.shotsLimit, shots).toFloat
shotsMultiplier * (if (event.shotsNatLog > 0f) {
math.log(math.pow(partialShots, event.shotsNatLog) + 2d).toFloat
} else {
partialShots
})
}
val modifiedAmountReward: Float = event.amountMultiplier * weaponStat.amount.toFloat
event.base + modifiedShotsReward + modifiedAmountReward
} else {
0f
}
}
.getOrElse(
canNotFindEventDefaultValue.getOrElse(sep.canNotFindEventDefaultValue.toFloat)
)
weaponStat.copy(contributions = rewards)
}
} }

View file

@ -2,7 +2,6 @@
package net.psforever.objects.zones.exp package net.psforever.objects.zones.exp
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import net.psforever.objects.avatar.scoring.EquipmentStat import net.psforever.objects.avatar.scoring.EquipmentStat
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.sourcing.VehicleSource import net.psforever.objects.sourcing.VehicleSource
@ -11,8 +10,6 @@ import net.psforever.types.Vector3
import net.psforever.util.Database.ctx import net.psforever.util.Database.ctx
import net.psforever.util.Database.ctx._ import net.psforever.util.Database.ctx._
import scala.util.Success
object ToDatabase { object ToDatabase {
/** /**
* Insert an entry into the database's `killactivity` table. * Insert an entry into the database's `killactivity` table.
@ -57,7 +54,7 @@ object ToDatabase {
position: Vector3, position: Vector3,
exp: Long exp: Long
): Unit = { ): Unit = {
ctx.run(query[persistence.Assistactivity] ctx.run(query[persistence.Killactivity]
.insert( .insert(
_.killerId -> lift(avatarId), _.killerId -> lift(avatarId),
_.victimId -> lift(victimId), _.victimId -> lift(victimId),
@ -115,6 +112,10 @@ object ToDatabase {
_.assists -> lift(0), _.assists -> lift(0),
_.sessionId -> lift(-1L) _.sessionId -> lift(-1L)
) )
.onConflictUpdate(_.avatarId, _.weaponId, _.sessionId)(
(t, e) => t.shotsFired -> (t.shotsFired + e.shotsFired),
(t, e) => t.shotsLanded -> (t.shotsLanded + e.shotsLanded)
)
) )
} }
@ -151,4 +152,66 @@ object ToDatabase {
) )
) )
} }
/**
* Insert an entry into the database's `ntuactivity` table.
* This table monitors experience earned through NTU silo operations and
* first time event entity interactions (zone and building set to 0).
*/
def reportNtuActivity(
avatarId: Long,
zoneId: Int,
buildingId: Int,
experience: Long
): Unit = {
ctx.run(query[persistence.Ntuactivity]
.insert(
_.avatarId -> lift(avatarId),
_.zoneId -> lift(zoneId),
_.buildingId -> lift(buildingId),
_.exp -> lift(experience)
)
.onConflictUpdate(_.avatarId, _.zoneId, _.buildingId)(
(t, e) => t.exp -> (t.exp + e.exp)
)
)
}
/**
* Insert an entry into the database's `kdasession` table
* to specifically update the revive counter for the current session.
*/
def reportRespawns(
avatarId: Long,
reviveCount: Int
): Unit = {
ctx.run(query[persistence.Kdasession]
.insert(
_.avatarId -> lift(avatarId),
_.revives -> lift(reviveCount),
_.sessionId -> lift(-1)
)
)
}
/**
* Insert an entry into the database's `buildingCapture` table.
*/
def reportFacilityCapture(
avatarId: Long,
zoneId: Int,
buildingId: Int,
exp: Long,
expType: String
): Unit = {
ctx.run(query[persistence.Buildingcapture]
.insert(
_.avatarId -> lift(avatarId),
_.zoneId -> lift(zoneId),
_.buildingId -> lift(buildingId),
_.exp -> lift(exp),
_.expType -> lift(expType)
)
)
}
} }

View file

@ -3,7 +3,7 @@ package net.psforever.objects.zones.exp.rec
import net.psforever.objects.sourcing.SourceUniqueness import net.psforever.objects.sourcing.SourceUniqueness
import net.psforever.objects.vital.InGameActivity import net.psforever.objects.vital.InGameActivity
import net.psforever.objects.zones.exp.{ContributionStats, KillContributions, WeaponStats} import net.psforever.objects.zones.exp.{ContributionStats, KillContributions, Support, WeaponStats}
import net.psforever.types.PlanetSideEmpire import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable import scala.collection.mutable
@ -58,23 +58,32 @@ class CombinedHealthAndArmorContributionProcess(
} }
.map { .map {
case (id, entry) => case (id, entry) =>
var totalShots: Int = 0
var totalAmount: Int = 0
var mostRecentTime: Long = 0
val groupedWeapons = entry.weapons val groupedWeapons = entry.weapons
.groupBy(_.equipment) .groupBy(_.equipment)
.map { .map {
case (weaponId, weaponEntries) => case (weaponContext, weaponEntries) =>
val specificEntries = weaponEntries.filter(_.equipment == weaponId) val specificEntries = weaponEntries.filter(_.equipment == weaponContext)
val amount = specificEntries.foldLeft(0)(_ + _.amount) val amount = specificEntries.foldLeft(0)(_ + _.amount)
totalAmount = totalAmount + amount
val shots = specificEntries.foldLeft(0)(_ + _.shots) val shots = specificEntries.foldLeft(0)(_ + _.shots)
WeaponStats(weaponId, amount, shots, specificEntries.maxBy(_.time).time, 1f) totalShots = totalShots + shots
val time = specificEntries.maxBy(_.time).time
mostRecentTime = math.max(mostRecentTime, time)
Support.calculateSupportExperience(
event = "support-heal",
WeaponStats(weaponContext, amount, shots, time, 1f)
)
} }
.toSeq .toSeq
(id, ContributionStats( (id, entry.copy(
player = entry.player,
weapons = groupedWeapons, weapons = groupedWeapons,
amount = entry.amount + entry.amount, amount = totalAmount,
total = entry.total + entry.total, total = math.max(entry.total, totalAmount),
shots = groupedWeapons.foldLeft(0)(_ + _.shots), shots = totalShots,
time = groupedWeapons.maxBy(_.time).time time = mostRecentTime
)) ))
} }
} }

View file

@ -3,15 +3,15 @@ package net.psforever.objects.zones.exp.rec
import net.psforever.objects.sourcing.SourceUniqueness import net.psforever.objects.sourcing.SourceUniqueness
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairingActivity} import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairingActivity}
import net.psforever.objects.zones.exp.ContributionStats import net.psforever.objects.zones.exp.{ContributionStats, Support, WeaponStats}
import net.psforever.types.PlanetSideEmpire import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable import scala.collection.mutable
//noinspection ScalaUnusedSymbol
class MachineRecoveryExperienceContributionProcess( class MachineRecoveryExperienceContributionProcess(
private val faction : PlanetSideEmpire.Value, private val faction : PlanetSideEmpire.Value,
private val contributions: Map[SourceUniqueness, List[InGameActivity]], private val contributions: Map[SourceUniqueness, List[InGameActivity]],
eventOutputType: String,
private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer() private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer()
) extends RecoveryExperienceContributionProcess(faction, contributions) { ) extends RecoveryExperienceContributionProcess(faction, contributions) {
def submit(history: List[InGameActivity]): Unit = { def submit(history: List[InGameActivity]): Unit = {
@ -65,11 +65,9 @@ class MachineRecoveryExperienceContributionProcess(
.map { case (wrapper, entries) => .map { case (wrapper, entries) =>
val size = entries.size val size = entries.size
val newTime = entries.maxBy(_.time).time val newTime = entries.maxBy(_.time).time
entries.head.copy( Support.calculateSupportExperience(
shots = size, eventOutputType,
amount = entries.foldLeft(0)(_ + _.amount), WeaponStats(wrapper, size, entries.foldLeft(0)(_ + _.amount), newTime, 1f)
contributions = (10 + size).toFloat,
time = newTime
) )
} }
.toSeq .toSeq

View file

@ -1,16 +0,0 @@
package net.psforever.persistence
import org.joda.time.LocalDateTime
case class Assistactivity(
index: Int,
killerId: Long,
victimId: Long,
weaponId: Int,
zoneId: Int,
px: Int, //Position.x * 1000
py: Int, //Position.y * 1000
pz: Int, //Position.z * 1000
exp: Long,
timestamp: LocalDateTime = LocalDateTime.now()
)

View file

@ -0,0 +1,95 @@
// Copyright (c) 2023 PSForever
package net.psforever.persistence
import org.joda.time.LocalDateTime
case class Assistactivity(
index: Int,
killerId: Long,
victimId: Long,
weaponId: Int,
zoneId: Int,
px: Int, //Position.x * 1000
py: Int, //Position.y * 1000
pz: Int, //Position.z * 1000
exp: Long,
timestamp: LocalDateTime = LocalDateTime.now()
)
case class Buildingcapture(
index: Int,
avatarId: Long,
zoneId: Int,
buildingId: Int,
exp: Long,
expType: String,
timestamp: LocalDateTime = LocalDateTime.now()
)
case class Kdasession (
avatarId: Long,
sessionId: Int,
kills: Int,
deaths: Int,
assists: Int,
revives: Int
)
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,
timestamp: LocalDateTime = LocalDateTime.now()
)
case class Machinedestroyed(
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()
)
case class Ntuactivity (
avatarId: Long,
zoneId: Int,
buildingId: Int,
exp: Long
)
case class Supportactivity(
index: Int,
userId: Long,
targetId: Long,
targetExosuit: Int,
interactionType: Int,
implementType: Int,
intermediateType: Int,
exp: Long,
timestamp: LocalDateTime = LocalDateTime.now()
)
case class Weaponstatsession(
avatarId: Long,
weaponId: Int,
shotsFired: Int,
shotsLanded: Int,
kills: Int,
assists: Int,
sessionId: Long
)

View file

@ -1,19 +0,0 @@
// Copyright (c) 2022 PSForever
package net.psforever.persistence
import org.joda.time.LocalDateTime
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,
timestamp: LocalDateTime = LocalDateTime.now()
)

View file

@ -1,19 +0,0 @@
// Copyright (c) 2022 PSForever
package net.psforever.persistence
import org.joda.time.LocalDateTime
case class Machinedestroyed(
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()
)

View file

@ -1,16 +0,0 @@
// Copyright (c) 2022 PSForever
package net.psforever.persistence
import org.joda.time.LocalDateTime
case class Supportactivity(
index: Int,
userId: Long,
targetId: Long,
targetExosuit: Int,
interactionType: Int,
implementType: Int,
intermediateType: Int,
exp: Long,
timestamp: LocalDateTime = LocalDateTime.now()
)

View file

@ -1,12 +0,0 @@
// 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
)

View file

@ -82,7 +82,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
entryManagementBehaviors entryManagementBehaviors
.orElse { .orElse {
case RemoverActor.AddTask(obj, zone, duration) => case RemoverActor.AddTask(obj, zone, duration) =>
val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toNanos) val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toMillis)
if (InclusionTest(entry) && !secondHeap.exists(test => sameEntryComparator.Test(test, entry))) { if (InclusionTest(entry) && !secondHeap.exists(test => sameEntryComparator.Test(test, entry))) {
InitialJob(entry) InitialJob(entry)
if (entry.duration == 0) { if (entry.duration == 0) {
@ -120,7 +120,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
case RemoverActor.StartDelete() => case RemoverActor.StartDelete() =>
firstTask.cancel() firstTask.cancel()
secondTask.cancel() secondTask.cancel()
val now: Long = System.nanoTime val now: Long = System.currentTimeMillis()
val (in, out) = firstHeap.partition(entry => { val (in, out) = firstHeap.partition(entry => {
now - entry.time >= entry.duration now - entry.time >= entry.duration
}) })
@ -229,19 +229,19 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
* this new entry is always set to last for the duration of the second pool * this new entry is always set to last for the duration of the second pool
*/ */
private def RepackageEntry(entry: RemoverActor.Entry): RemoverActor.Entry = { private def RepackageEntry(entry: RemoverActor.Entry): RemoverActor.Entry = {
RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toNanos) RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toMillis)
} }
/** /**
* Common function to reset the first task's delayed execution. * Common function to reset the first task's delayed execution.
* Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool. * Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool.
* @param now the time (in nanoseconds); * @param now the time (in milliseconds);
* defaults to the current time (in nanoseconds) * defaults to the current time (in milliseconds)
*/ */
def RetimeFirstTask(now: Long = System.nanoTime): Unit = { def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = {
firstTask.cancel() firstTask.cancel()
if (firstHeap.nonEmpty) { if (firstHeap.nonEmpty) {
val short_timeout: FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)) nanoseconds val short_timeout: FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)).milliseconds
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
firstTask = context.system.scheduler.scheduleOnce(short_timeout, self, RemoverActor.StartDelete()) firstTask = context.system.scheduler.scheduleOnce(short_timeout, self, RemoverActor.StartDelete())
} }
@ -274,14 +274,14 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
/** /**
* Default time for entries waiting in the first list. * Default time for entries waiting in the first list.
* Override. * Override.
* @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds) * @return the time as a `FiniteDuration` object (to be later transformed into milliseconds)
*/ */
def FirstStandardDuration: FiniteDuration def FirstStandardDuration: FiniteDuration
/** /**
* Default time for entries waiting in the second list. * Default time for entries waiting in the second list.
* Override. * Override.
* @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds) * @return the time as a `FiniteDuration` object (to be later transformed into milliseconds)
*/ */
def SecondStandardDuration: FiniteDuration def SecondStandardDuration: FiniteDuration
@ -322,7 +322,7 @@ object RemoverActor extends SupportActorCaseConversions {
* Internally, all entries have a "time created" field. * Internally, all entries have a "time created" field.
* @param _obj the target * @param _obj the target
* @param _zone the zone in which this target is registered * @param _zone the zone in which this target is registered
* @param _duration how much longer the target will exist in its current state (in nanoseconds) * @param _duration how much longer the target will exist in its current state (in milliseconds)
*/ */
case class Entry(_obj: PlanetSideGameObject, _zone: Zone, _duration: Long) case class Entry(_obj: PlanetSideGameObject, _zone: Zone, _duration: Long)
extends SupportActor.Entry(_obj, _zone, _duration) extends SupportActor.Entry(_obj, _zone, _duration)
@ -332,7 +332,7 @@ object RemoverActor extends SupportActorCaseConversions {
* @see `FirstStandardDuration` * @see `FirstStandardDuration`
* @param obj the target * @param obj the target
* @param zone the zone in which this target is registered * @param zone the zone in which this target is registered
* @param duration how much longer the target will exist in its current state (in nanoseconds); * @param duration how much longer the target will exist in its current state (in milliseconds);
* a default time duration is provided by implementation * a default time duration is provided by implementation
*/ */
case class AddTask(obj: PlanetSideGameObject, zone: Zone, duration: Option[FiniteDuration] = None) case class AddTask(obj: PlanetSideGameObject, zone: Zone, duration: Option[FiniteDuration] = None)

View file

@ -456,6 +456,15 @@ class AvatarService(zone: Zone) extends Actor {
) )
) )
case AvatarAction.FacilityCaptureRewards(building_id, zone_number, exp) =>
AvatarEvents.publish(
AvatarServiceResponse(
s"/$forChannel/Avatar",
Service.defaultPlayerGUID,
AvatarResponse.FacilityCaptureRewards(building_id, zone_number, exp)
)
)
case _ => () case _ => ()
} }

View file

@ -158,6 +158,7 @@ object AvatarAction {
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action
final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Action final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Action
final case class AwardCep(charId: Long, bep: Long) extends Action final case class AwardCep(charId: Long, bep: Long) extends Action
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action
final case class TeardownConnection() extends Action final case class TeardownConnection() extends Action
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action

View file

@ -129,4 +129,5 @@ object AvatarResponse {
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response
final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Response final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Response
final case class AwardCep(charId: Long, bep: Long) extends Response final case class AwardCep(charId: Long, bep: Long) extends Response
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response
} }

View file

@ -49,8 +49,7 @@ class DoorCloseActor() extends Actor {
}) })
if (openDoors.nonEmpty) { if (openDoors.nonEmpty) {
val short_timeout: FiniteDuration = val short_timeout: FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)).milliseconds
math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors()) doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
} }
@ -67,7 +66,7 @@ class DoorCloseActor() extends Actor {
* and newer entries are always added to the end of the main `List`, * and newer entries are always added to the end of the main `List`,
* processing in order is always correct. * processing in order is always correct.
* @param list the `List` of entries to divide * @param list the `List` of entries to divide
* @param now the time right now (in nanoseconds) * @param now the time right now (in milliseconds)
* @see `List.partition` * @see `List.partition`
* @return a `Tuple` of two `Lists`, whose qualifications are explained above * @return a `Tuple` of two `Lists`, whose qualifications are explained above
*/ */
@ -84,7 +83,7 @@ class DoorCloseActor() extends Actor {
* a `List` of elements that have exceeded the time limit, * a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit. * and a `List` of elements that still satisfy the time limit.
* @param iter the `Iterator` of entries to divide * @param iter the `Iterator` of entries to divide
* @param now the time right now (in nanoseconds) * @param now the time right now (in milliseconds)
* @param index a persistent record of the index where list division should occur; * @param index a persistent record of the index where list division should occur;
* defaults to 0 * defaults to 0
* @return the index where division will occur * @return the index where division will occur

View file

@ -17,7 +17,6 @@ import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
import java.util.concurrent.TimeUnit
import scala.concurrent.duration.{FiniteDuration, _} import scala.concurrent.duration.{FiniteDuration, _}
import scala.util.Random import scala.util.Random
@ -38,7 +37,7 @@ class HackCaptureActor extends Actor {
val duration = target.Definition.FacilityHackTime val duration = target.Definition.FacilityHackTime
target.HackedBy match { target.HackedBy match {
case Some(hackInfo) => case Some(hackInfo) =>
target.HackedBy = hackInfo.Duration(duration.toNanos) target.HackedBy = hackInfo.Duration(duration.toMillis)
case None => case None =>
log.error(s"Initial $target hack information is missing") log.error(s"Initial $target hack information is missing")
} }
@ -57,8 +56,8 @@ class HackCaptureActor extends Actor {
case HackCaptureActor.ProcessCompleteHacks() => case HackCaptureActor.ProcessCompleteHacks() =>
log.trace("Processing complete hacks") log.trace("Processing complete hacks")
clearTrigger.cancel() clearTrigger.cancel()
val now: Long = System.nanoTime val now: Long = System.currentTimeMillis()
val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toNanos) val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toMillis)
hackedObjects = stillHacked hackedObjects = stillHacked
finishedHacks.foreach { entry => finishedHacks.foreach { entry =>
val terminal = entry.target val terminal = entry.target
@ -213,9 +212,8 @@ class HackCaptureActor extends Actor {
private def RestartTimer(): Unit = { private def RestartTimer(): Unit = {
if (hackedObjects.nonEmpty) { if (hackedObjects.nonEmpty) {
val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.nanoTime())) val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.currentTimeMillis()))
val short_timeout: FiniteDuration = val short_timeout: FiniteDuration = math.max(1, hackEntry.duration.toMillis - (System.currentTimeMillis() - hackEntry.hack_timestamp)).milliseconds
math.max(1, hackEntry.duration.toNanos - (System.nanoTime - hackEntry.hack_timestamp)).nanoseconds
log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds") log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds")
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks()) clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks())
@ -229,7 +227,7 @@ object HackCaptureActor {
zone: Zone, zone: Zone,
unk1: Long, unk1: Long,
unk2: Long, unk2: Long,
startTime: Long = System.nanoTime() startTime: Long = System.currentTimeMillis()
) )
final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource) final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource)
@ -256,8 +254,7 @@ object HackCaptureActor {
17039360L 17039360L
case Some(Hackable.HackInfo(p, _, start, length)) => case Some(Hackable.HackInfo(p, _, start, length)) =>
// See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated // See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated
val hackTimeRemainingMS = val hackTimeRemainingMS = math.max(0, start + length - System.currentTimeMillis())
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
val startNum = p.Faction match { val startNum = p.Faction match {
case PlanetSideEmpire.TR => 0x10000 case PlanetSideEmpire.TR => 0x10000
case PlanetSideEmpire.NC => 0x20000 case PlanetSideEmpire.NC => 0x20000
@ -273,8 +270,8 @@ object HackCaptureActor {
entry1: HackCaptureActor.HackEntry, entry1: HackCaptureActor.HackEntry,
entry2: HackCaptureActor.HackEntry entry2: HackCaptureActor.HackEntry
): HackCaptureActor.HackEntry = { ): HackCaptureActor.HackEntry = {
val entry1TimeLeft = entry1.duration.toNanos - (now - entry1.hack_timestamp) val entry1TimeLeft = entry1.duration.toMillis - (now - entry1.hack_timestamp)
val entry2TimeLeft = entry2.duration.toNanos - (now - entry2.hack_timestamp) val entry2TimeLeft = entry2.duration.toMillis - (now - entry2.hack_timestamp)
if (entry1TimeLeft < entry2TimeLeft) { if (entry1TimeLeft < entry2TimeLeft) {
entry1 entry1
} else { } else {

View file

@ -29,15 +29,15 @@ class HackClearActor() extends Actor {
def receive: Receive = { def receive: Receive = {
case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) => case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) =>
val durationNanos = TimeUnit.NANOSECONDS.convert(duration, TimeUnit.SECONDS) val durationMillis = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS)
hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationNanos) hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationMillis)
// Restart the timer, in case this is the first object in the hacked objects list // Restart the timer, in case this is the first object in the hacked objects list
RestartTimer() RestartTimer()
case HackClearActor.TryClearHacks() => case HackClearActor.TryClearHacks() =>
clearTrigger.cancel() clearTrigger.cancel()
val now: Long = System.nanoTime val now: Long = System.currentTimeMillis()
//TODO we can just walk across the list of doors and extract only the first few entries //TODO we can just walk across the list of doors and extract only the first few entries
val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now) val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now)
hackedObjects = stillHackedObjects hackedObjects = stillHackedObjects
@ -75,12 +75,12 @@ class HackClearActor() extends Actor {
private def RestartTimer(): Unit = { private def RestartTimer(): Unit = {
if (hackedObjects.nonEmpty) { if (hackedObjects.nonEmpty) {
val now = System.nanoTime() val now = System.currentTimeMillis()
val (_/*unhackObjects*/, stillHackedObjects) = PartitionEntries(hackedObjects, now) val (_/*unhackObjects*/, stillHackedObjects) = PartitionEntries(hackedObjects, now)
stillHackedObjects.headOption match { stillHackedObjects.headOption match {
case Some(hackEntry) => case Some(hackEntry) =>
val short_timeout: FiniteDuration = math.max(1, hackEntry.duration - (now - hackEntry.time)) nanoseconds val short_timeout: FiniteDuration = math.max(1, hackEntry.duration - (now - hackEntry.time)).milliseconds
log.debug( log.debug(
s"HackClearActor: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds" s"HackClearActor: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds"
@ -102,7 +102,7 @@ class HackClearActor() extends Actor {
* and newer entries are always added to the end of the main `List`, * and newer entries are always added to the end of the main `List`,
* processing in order is always correct. * processing in order is always correct.
* @param list the `List` of entries to divide * @param list the `List` of entries to divide
* @param now the time right now (in nanoseconds) * @param now the time right now (in milliseconds)
* @see `List.partition` * @see `List.partition`
* @return a `Tuple` of two `Lists`, whose qualifications are explained above * @return a `Tuple` of two `Lists`, whose qualifications are explained above
*/ */
@ -119,7 +119,7 @@ class HackClearActor() extends Actor {
* a `List` of elements that have exceeded the time limit, * a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit. * and a `List` of elements that still satisfy the time limit.
* @param iter the `Iterator` of entries to divide * @param iter the `Iterator` of entries to divide
* @param now the time right now (in nanoseconds) * @param now the time right now (in milliseconds)
* @param index a persistent record of the index where list division should occur; * @param index a persistent record of the index where list division should occur;
* defaults to 0 * defaults to 0
* @return the index where division will occur * @return the index where division will occur
@ -158,7 +158,7 @@ object HackClearActor {
unk1: Long, unk1: Long,
unk2: Long, unk2: Long,
duration: Int, duration: Int,
time: Long = System.nanoTime() time: Long = System.currentTimeMillis()
) )
/** /**
@ -185,7 +185,7 @@ object HackClearActor {
* @param target the server object * @param target the server object
* @param zone the zone in which the object resides * @param zone the zone in which the object resides
* @param time when the object was hacked * @param time when the object was hacked
* @param duration The hack duration in nanoseconds * @param duration The hack duration in milliseconds
* @see `ObjectIsHacked` * @see `ObjectIsHacked`
*/ */
private final case class HackEntry( private final case class HackEntry(

View file

@ -96,7 +96,7 @@ abstract class SupportActor[A <: SupportActor.Entry] extends Actor {
object SupportActor { object SupportActor {
class Entry(val obj: PlanetSideGameObject, val zone: Zone, val duration: Long) { class Entry(val obj: PlanetSideGameObject, val zone: Zone, val duration: Long) {
val time: Long = System.nanoTime val time: Long = System.currentTimeMillis()
} }
/** /**

View file

@ -50,8 +50,8 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
entryManagementBehaviors entryManagementBehaviors
.orElse { .orElse {
case TurretUpgrader.AddTask(turret, zone, upgrade, duration) => case TurretUpgrader.AddTask(turret, zone, upgrade, duration) =>
val lengthOfTime = duration.getOrElse(TurretUpgrader.StandardUpgradeLifetime).toNanos val lengthOfTime = duration.getOrElse(TurretUpgrader.StandardUpgradeLifetime).toMillis
if (lengthOfTime > (1 second).toNanos) { //don't even bother if it's too short; it'll revert near instantly if (lengthOfTime > (1 second).toMillis) { //don't even bother if it's too short; it'll revert near instantly
val entry = CreateEntry(turret, zone, TurretUpgrade.None, lengthOfTime) val entry = CreateEntry(turret, zone, TurretUpgrade.None, lengthOfTime)
UpgradeTurretAmmo(CreateEntry(turret, zone, upgrade, lengthOfTime)) UpgradeTurretAmmo(CreateEntry(turret, zone, upgrade, lengthOfTime))
if (list.isEmpty) { if (list.isEmpty) {
@ -73,7 +73,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
case TurretUpgrader.Downgrade() => case TurretUpgrader.Downgrade() =>
task.cancel() task.cancel()
val now: Long = System.nanoTime val now: Long = System.currentTimeMillis()
val (in, out) = val (in, out) =
list.partition(entry => { list.partition(entry => {
now - entry.time >= entry.duration now - entry.time >= entry.duration
@ -85,10 +85,10 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
case _ => ; case _ => ;
} }
def RetimeFirstTask(now: Long = System.nanoTime): Unit = { def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = {
task.cancel() task.cancel()
if (list.nonEmpty) { if (list.nonEmpty) {
val short_timeout: FiniteDuration = math.max(1, list.head.duration - (now - list.head.time)) nanoseconds val short_timeout: FiniteDuration = math.max(1, list.head.duration - (now - list.head.time)).milliseconds
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
task = context.system.scheduler.scheduleOnce(short_timeout, self, TurretUpgrader.Downgrade()) task = context.system.scheduler.scheduleOnce(short_timeout, self, TurretUpgrader.Downgrade())
} }
@ -249,7 +249,7 @@ object TurretUpgrader extends SupportActorCaseConversions {
* @param _obj the target * @param _obj the target
* @param _zone the zone in which this target is registered * @param _zone the zone in which this target is registered
* @param upgrade the next upgrade state for this turret * @param upgrade the next upgrade state for this turret
* @param _duration how much longer the target will exist in its current state (in nanoseconds) * @param _duration how much longer the target will exist in its current state (in milliseconds)
*/ */
case class Entry(_obj: PlanetSideGameObject, _zone: Zone, upgrade: TurretUpgrade.Value, _duration: Long) case class Entry(_obj: PlanetSideGameObject, _zone: Zone, upgrade: TurretUpgrade.Value, _duration: Long)
extends SupportActor.Entry(_obj, _zone, _duration) extends SupportActor.Entry(_obj, _zone, _duration)

View file

@ -150,9 +150,6 @@ case class GameConfig(
instantAction: InstantActionConfig, instantAction: InstantActionConfig,
amenityAutorepairRate: Float, amenityAutorepairRate: Float,
amenityAutorepairDrainRate: Float, amenityAutorepairDrainRate: Float,
bepRate: Double,
cepRate: Double,
maximumCepPerSquadSize: Seq[Int],
newAvatar: NewAvatar, newAvatar: NewAvatar,
hart: HartConfig, hart: HartConfig,
sharedMaxCooldown: Boolean, sharedMaxCooldown: Boolean,
@ -162,7 +159,8 @@ case class GameConfig(
cavernRotation: CavernRotationConfig, cavernRotation: CavernRotationConfig,
savedMsg: SavedMessageEvents, savedMsg: SavedMessageEvents,
playerDraw: PlayerStateDrawSettings, playerDraw: PlayerStateDrawSettings,
doorsCanBeOpenedByMedAppFromThisDistance: Float doorsCanBeOpenedByMedAppFromThisDistance: Float,
experience: Experience
) )
case class InstantActionConfig( case class InstantActionConfig(
@ -238,3 +236,51 @@ case class PlayerStateDrawSettings(
assert(ranges.nonEmpty) assert(ranges.nonEmpty)
assert(ranges.size == delays.size) assert(ranges.size == delays.size)
} }
case class Experience(
shortContributionTime: Long,
longContributionTime: Long,
bep: BattleExperiencePoints,
sep: SupportExperiencePoints,
cep: CommandExperiencePoints
) {
assert(shortContributionTime < longContributionTime)
}
case class BattleExperiencePoints(
base: BattleExperiencePointsBase,
rate: Float
)
case class BattleExperiencePointsBase(
bopsMultiplier: Long,
asMax: Long,
withKills: Long,
asMounted: Long,
mature: Long
)
case class SupportExperiencePoints(
rate: Float,
ntuSiloDepositReward: Long,
canNotFindEventDefaultValue: Long,
events: Seq[SupportExperienceEvent]
)
case class SupportExperienceEvent(
name: String,
base: Long,
shotsMultiplier: Float = 0f,
shotsNatLog: Double = 0f,
shotsLimit: Int = 50,
shotsCutoff: Int = 50,
amountMultiplier: Float = 0f
)
case class CommandExperiencePoints(
rate: Float,
lluCarrierModifier: Float,
maximumPerSquadSize: Seq[Int],
squadSizeLimitOverflow: Int,
squadSizeLimitOverflowMultiplier: Float
)

View file

@ -624,7 +624,7 @@ class PlayerControlDeathStandingTest extends ActorTest {
// override def AvatarEvents = avatarProbe.ref // override def AvatarEvents = avatarProbe.ref
// override def Activity = activityProbe.ref // override def Activity = activityProbe.ref
// } // }
// zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.nanoTime()}") // zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.currentTimeMillis()}")
// //
// player1.Zone = zone // player1.Zone = zone
// player1.Spawn() // player1.Spawn()