From 77066f962e96558ea0f71f72037ab6507e3bbbc6 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Fri, 6 Oct 2023 23:58:59 -0400 Subject: [PATCH] command experience calculations upon facility capture or resecure --- .../resources/db/migration/V008__Scoring.sql | 527 ++++++++++++++---- .../resources/db/migration/V010__Scoring2.sql | 380 ------------- .../actor/objects/VehicleSpawnPadTest.scala | 6 +- src/main/resources/application.conf | 166 +++++- .../actors/session/AvatarActor.scala | 25 +- .../support/SessionAvatarHandlers.scala | 110 ++-- .../actors/session/support/SessionData.scala | 4 +- .../session/support/ZoningOperations.scala | 2 + .../psforever/objects/GlobalDefinitions.scala | 3 +- .../scala/net/psforever/objects/Player.scala | 9 +- .../scala/net/psforever/objects/Tool.scala | 2 +- .../objects/avatar/scoring/Life.scala | 7 +- .../objects/avatar/scoring/ScoreCard.scala | 32 +- .../converter/CaptureFlagConverter.scala | 2 +- .../serverobject/hackable/Hackable.scala | 4 +- .../resourcesilo/ResourceSiloControl.scala | 9 +- .../resourcesilo/ResourceSiloDefinition.scala | 4 + .../serverobject/structures/Building.scala | 3 +- .../FacilityHackParticipation.scala | 99 ++-- .../MajorFacilityHackParticipation.scala | 226 +++++--- .../TowerHackParticipation.scala | 122 ++-- .../turret/FacilityTurretControl.scala | 2 +- .../vehicles/AntTransferBehavior.scala | 6 +- .../vehicles/control/VehicleControl.scala | 2 +- .../objects/vital/InGameHistory.scala | 3 +- .../objects/zones/exp/KillAssists.scala | 16 +- .../objects/zones/exp/KillContributions.scala | 294 ++++++---- .../psforever/objects/zones/exp/Support.scala | 61 +- .../objects/zones/exp/ToDatabase.scala | 71 ++- ...nedHealthAndArmorContributionProcess.scala | 29 +- ...ecoveryExperienceContributionProcess.scala | 12 +- .../persistence/Assistactivity.scala | 16 - .../net/psforever/persistence/KdaExp.scala | 95 ++++ .../psforever/persistence/Killactivity.scala | 19 - .../persistence/Machinedestroyed.scala | 19 - .../persistence/Supportactivity.scala | 16 - .../persistence/Weaponstatsession.scala | 12 - .../net/psforever/services/RemoverActor.scala | 22 +- .../services/avatar/AvatarService.scala | 9 + .../avatar/AvatarServiceMessage.scala | 1 + .../avatar/AvatarServiceResponse.scala | 1 + .../local/support/DoorCloseActor.scala | 7 +- .../local/support/HackCaptureActor.scala | 21 +- .../local/support/HackClearActor.scala | 18 +- .../services/support/SupportActor.scala | 2 +- .../vehicle/support/TurretUpgrader.scala | 12 +- .../scala/net/psforever/util/Config.scala | 54 +- .../scala/objects/PlayerControlTest.scala | 2 +- 48 files changed, 1544 insertions(+), 1020 deletions(-) delete mode 100644 server/src/main/resources/db/migration/V010__Scoring2.sql delete mode 100644 src/main/scala/net/psforever/persistence/Assistactivity.scala create mode 100644 src/main/scala/net/psforever/persistence/KdaExp.scala delete mode 100644 src/main/scala/net/psforever/persistence/Killactivity.scala delete mode 100644 src/main/scala/net/psforever/persistence/Machinedestroyed.scala delete mode 100644 src/main/scala/net/psforever/persistence/Supportactivity.scala delete mode 100644 src/main/scala/net/psforever/persistence/Weaponstatsession.scala diff --git a/server/src/main/resources/db/migration/V008__Scoring.sql b/server/src/main/resources/db/migration/V008__Scoring.sql index 40987c497..3453da654 100644 --- a/server/src/main/resources/db/migration/V008__Scoring.sql +++ b/server/src/main/resources/db/migration/V008__Scoring.sql @@ -17,9 +17,10 @@ CREATE TABLE IF NOT EXISTS "sessionnumber" ( CREATE TABLE IF NOT EXISTS "killactivity" ( "index" SERIAL PRIMARY KEY NOT NULL, - "victim_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_mounted" INT NOT NULL DEFAULT 0, "weapon_id" SMALLINT NOT NULL, "zone_id" SMALLINT NOT NULL, "px" INT NOT NULL, @@ -33,7 +34,7 @@ CREATE TABLE IF NOT EXISTS "kda" ( "avatar_id" INT NOT NULL REFERENCES avatar (id), "kills" 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) ); @@ -42,7 +43,7 @@ CREATE TABLE IF NOT EXISTS "kdasession" ( "session_id" INT NOT NULL, "kills" 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) ); @@ -73,89 +74,144 @@ CREATE TABLE IF NOT EXISTS "legacykills" ( UNIQUE(avatar_id) ); -CREATE VIEW kdacampaign AS ( - SELECT - session.avatar_id, - SUM(session.kills) AS kills, - SUM(session.deaths) AS deaths, - SUM(session.assists) AS assists, - COUNT(session.avatar_id) AS numberOfSessions - FROM ( - SELECT avatar_id, session_id, kills, deaths, assists - FROM kdasession - UNION ALL - SELECT avatar_id, 0, kills, deaths, assists - FROM kda - ) AS session - LEFT JOIN kda - ON kda.avatar_id = session.avatar_id - GROUP BY session.avatar_id +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 VIEW weaponstatcampaign AS ( - SELECT - weaponstat.avatar_id, - weaponstat.weapon_id, - SUM(session.shots_fired) AS shots_fired, - SUM(session.shots_landed) AS shots_landed, - SUM(session.kills) AS kills, - SUM(session.assists) AS assists, - COUNT(session.session_id) AS numberOfSessions - FROM ( - SELECT avatar_id, weapon_id, session_id, shots_fired, shots_landed, kills, assists - 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 "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 ); -/* -Procedure for initializing and increasing the session number. -Always indexes a new session. -*/ -CREATE OR REPLACE PROCEDURE proc_sessionnumber_initAndOrIncrease -(avatarId IN Int, number OUT Int) -AS -$$ -BEGIN - SELECT (MAX(session_id,0)+1) INTO number - FROM sessionnumber - WHERE avatar_id = avatarId; - INSERT INTO sessionnumber - VALUES (avatarId, number); -END; -$$ LANGUAGE plpgsql; +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 "ntuactivity" ( + "avatar_id" INT NOT NULL REFERENCES avatar (id), + "zone_id" INT NOT NULL, + "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. Index for a new session only if last session was created more than one hour ago. */ -CREATE OR REPLACE PROCEDURE proc_sessionnumber_initAndOrIncreasePerHour -(avatarId IN Int, number OUT Int, nextNumber OUT Int) +CREATE OR REPLACE FUNCTION proc_sessionnumber_test +(avatarId integer) +RETURNS integer AS $$ DECLARE time TIMESTAMP; +DECLARE number integer; BEGIN - SELECT MAX(session_id,0) INTO number + SELECT MAX(session_id) INTO number FROM sessionnumber WHERE avatar_id = avatarId; - SELECT timestamp INTO time + SELECT COALESCE(timestamp) INTO time FROM sessionnumber 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; INSERT INTO sessionnumber - VALUES (avatarId, number); + VALUES (avatarId, nextNumber); ELSE nextNumber := number; UPDATE sessionnumber SET timestamp = CURRENT_TIMESTAMP WHERE avatar_id = avatarId AND session_id = number; END IF; + RETURN nextNumber; END; $$ LANGUAGE plpgsql; @@ -163,24 +219,35 @@ $$ LANGUAGE plpgsql; Procedure for accessing any existing session number. Actually polls the previous session number from the account table. */ -CREATE OR REPLACE PROCEDURE proc_sessionnumber_get -(avatarId IN Int, number OUT Int) +CREATE OR REPLACE FUNCTION proc_sessionnumber_get +(avatarId integer) +RETURNS integer AS $$ +DECLARE sessionId integer; +DECLARE number integer; BEGIN - SELECT COALESCE(session_id,0) INTO number + 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; + RETURN number; END; $$ LANGUAGE plpgsql; /* 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 -(avatarId IN Int) +CREATE OR REPLACE FUNCTION proc_kda_addEntryIfNone +(avatarId integer) +RETURNS integer AS $$ +DECLARE out integer; BEGIN IF EXISTS( SELECT * @@ -189,43 +256,115 @@ BEGIN HAVING COUNT(*) = 0) THEN INSERT INTO kda (avatar_id) VALUES (avatarId); + out := 1; + ELSE + out := 0; END IF; + RETURN out; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE PROCEDURE proc_weaponstat_addEntryIfNone -(avatarId IN Int, weaponId IN Int) +CREATE OR REPLACE FUNCTION proc_weaponstat_addEntryIfNone +(avatarId integer, weaponId integer) +RETURNS integer AS $$ -DECLARE sessionId Int; +DECLARE out integer; BEGIN - SELECT proc_sessionnumber_get(avatarId, sessionId); - IF EXISTS( - SELECT * + IF NOT EXISTS( + SELECT avatar_id, weapon_id FROM weaponstat WHERE avatar_id = avatarId AND weapon_id = weaponId - HAVING COUNT(*) = 0) THEN - INSERT INTO weaponstat (avatar_id, session_id, weapon_id) - VALUES (avatarId, sessionId, weaponId); + GROUP BY avatar_id, weapon_id + ) THEN + INSERT INTO weaponstat (avatar_id, weapon_id) + VALUES (avatarId, weaponId); + out := 1; + ELSE + out := 0; END IF; + RETURN out; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE PROCEDURE proc_weaponstatsession_addEntryIfNone -(avatarId IN Int, weaponId IN Int) +CREATE OR REPLACE FUNCTION proc_weaponstatsession_addEntryIfNoneWithSessionId +(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 $$ DECLARE sessionId Int; BEGIN - SELECT proc_sessionnumber_get(avatarId, sessionId); - IF EXISTS( - SELECT * - FROM weaponstatsession - WHERE avatar_id = avatarId AND weapon_id = weaponId - HAVING COUNT(*) = 0) THEN - INSERT INTO weaponstatsession (avatar_id, session_id, weapon_id) - VALUES (avatarId, sessionId, weaponId); + sessionId := proc_sessionnumber_get(avatarId); + RETURN proc_weaponstatsession_addEntryIfNoneWithSessionId(avatarId, weaponId, sessionId); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION proc_expbuildingcapture_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 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; + 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; $$ LANGUAGE plpgsql; @@ -243,11 +382,12 @@ DECLARE sessionId Int; DECLARE oldSessionId Int; BEGIN 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 BEGIN UPDATE account - SET sessionId = sessionId + SET session_id = sessionId WHERE id = OLD.id; INSERT INTO kdasession (avatar_id, session_id) VALUES (avatarId, sessionId); @@ -258,7 +398,7 @@ END; $$ LANGUAGE plpgsql; CREATE OR REPLACE TRIGGER psf_account_newSession -BEFORE UPDATE +AFTER UPDATE OF avatar_logged_in ON account FOR EACH ROW @@ -281,12 +421,13 @@ DECLARE victimSessionId Int; DECLARE killerId Int; DECLARE victimId Int; DECLARE weaponId Int; +DECLARE out integer; BEGIN killerId := NEW.killer_id; victimId := NEW.victim_id; weaponId := NEW.weapon_id; - SELECT proc_sessionnumber_get(killerId, killerSessionId); - SELECT proc_sessionnumber_get(victimId, victimSessionId); + killerSessionId := proc_sessionnumber_get(killerId); + victimSessionId := proc_sessionnumber_get(victimId); BEGIN UPDATE kdasession SET kills = kills + 1 @@ -298,6 +439,7 @@ BEGIN WHERE avatar_id = victimId AND session_id = victimSessionId; END; BEGIN + out := proc_weaponstatsession_addEntryIfNoneWithSessionId(killerId, weaponId, killerSessionId); UPDATE weaponstatsession SET kills = kills + 1 WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId; @@ -323,10 +465,12 @@ AS $$ DECLARE avatarId Int; DECLARE weaponId Int; +DECLARE out integer; BEGIN avatarId := NEW.avatar_id; weaponId := NEW.weapon_id; - SELECT proc_weaponstatsession_addEntryIfNone(avatarId, weaponId); + out := proc_weaponstatsession_addEntryIfNone(avatarId, weaponId); + RETURN NEW; END; $$ LANGUAGE plpgsql; @@ -346,9 +490,11 @@ RETURNS TRIGGER AS $$ DECLARE avatarId Int; +DECLARE out integer; BEGIN avatarId := NEW.avatar_id; - SELECT proc_kda_addEntryIfNone(avatarId); + out := proc_kda_addEntryIfNone(avatarId); + RETURN NEW; END; $$ LANGUAGE plpgsql; @@ -369,10 +515,12 @@ AS $$ DECLARE avatarId Int; DECLARE weaponId Int; +DECLARE out integer; BEGIN avatarId := NEW.avatar_id; weaponId := NEW.weapon_id; - SELECT proc_weaponstat_addEntryIfNone(avatarId, weaponId); + out := proc_weaponstat_addEntryIfNone(avatarId, weaponId); + RETURN NEW; END; $$ LANGUAGE plpgsql; @@ -382,10 +530,40 @@ ON weaponstat FOR EACH ROW 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, 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() RETURNS TRIGGER @@ -394,16 +572,18 @@ $$ DECLARE avatarId Int; DECLARE oldKills Int; DECLARE oldDeaths Int; -DECLARE oldAssists Int; +DECLARE oldRevives Int; +DECLARE out integer; BEGIN avatarId := OLD.avatar_id; oldKills := OLD.kills; oldDeaths := OLD.deaths; - oldAssists := OLD.assists; + oldRevives := OLD.Revives; + out := proc_kda_addEntryIfNone(avatarId); UPDATE kda SET kills = kills + oldKills, deaths = deaths + oldDeaths, - assists = assists + oldAssists + revives = revives + oldRevives WHERE avatar_id = avatarId; RETURN OLD; END; @@ -418,7 +598,6 @@ EXECUTE FUNCTION fn_kdasession_updateOnDelete(); /* 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. -This will fire mainly when called by the trigger for login (above). */ CREATE OR REPLACE FUNCTION fn_weaponstatsession_updateOnDelete() RETURNS TRIGGER @@ -430,6 +609,7 @@ DECLARE oldKills Int; DECLARE oldAssists Int; DECLARE oldFired Int; DECLARE oldLanded Int; +DECLARE out integer; BEGIN avatarId := OLD.avatar_id; weaponId := OLD.weapon_id; @@ -437,12 +617,14 @@ BEGIN oldAssists := OLD.assists; oldFired := OLD.shots_fired; oldLanded := OLD.shots_landed; + out := 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; @@ -451,3 +633,152 @@ BEFORE DELETE ON weaponstatsession FOR EACH ROW 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(); diff --git a/server/src/main/resources/db/migration/V010__Scoring2.sql b/server/src/main/resources/db/migration/V010__Scoring2.sql deleted file mode 100644 index 56e82eba9..000000000 --- a/server/src/main/resources/db/migration/V010__Scoring2.sql +++ /dev/null @@ -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(); diff --git a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala index 86423a0e5..6ca49eca0 100644 --- a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala +++ b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala @@ -231,17 +231,17 @@ object VehicleSpawnPadControlTest { override def SetupNumberPools(): Unit = {} } 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 // with actor probe setting // TODO(chord): Remove when Zone supports notification of booting being complete 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) - 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 = new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) pad.Owner.Faction = faction diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 4e89670d5..d6cc43a9b 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -75,15 +75,6 @@ game { 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 amenity-autorepair-rate = 1.0 @@ -226,6 +217,163 @@ game { # Don't ask. 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 { diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 26221e1c7..eb8848bdc 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -851,12 +851,18 @@ object AvatarActor { ) } - def updateToolDischargeFor(avatarId: Long, lives: Seq[Life]): Unit = { - lives - .flatMap { _.equipmentStats } - .foreach { stat => - zones.exp.ToDatabase.reportToolDischarge(avatarId, stat) - } + def updateToolDischargeFor(avatar: Avatar): Unit = { + updateToolDischargeFor(avatar.id, avatar.scorecard.CurrentLife) + } + + 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 = { @@ -1091,7 +1097,6 @@ class AvatarActor( .receiveSignal { case (_, PostStop) => AvatarActor.avatarNoLongerLoggedIn(account.id) - AvatarActor.updateToolDischargeFor(avatar.id.toLong, avatar.scorecard.CurrentLife +: avatar.scorecard.Lives) Behaviors.same } } @@ -1863,6 +1868,7 @@ class AvatarActor( AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool) } saveLockerFunc() + AvatarActor.updateToolDischargeFor(avatar) Behaviors.same } } @@ -3084,6 +3090,7 @@ class AvatarActor( } def updateDeaths(deathStat: Death): Unit = { + AvatarActor.updateToolDischargeFor(avatar) avatar.scorecard.rate(deathStat) val _session = session.get val zone = _session.zone @@ -3138,13 +3145,13 @@ class AvatarActor( def updateExperienceAndType(exp: Long): (Long, ExperienceType) = { val _session = session.get 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)) { (1.25f, ExperienceType.RabbitBall) } else { (1f, ExperienceType.Normal) } - ((exp * modifier * gameOpts.bepRate).toLong, msg) + ((exp * modifier * gameOpts.rate).toLong, msg) } def updateToolDischarge(stats: EquipmentStat): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 2af59a51e..f06347af6 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -5,6 +5,7 @@ import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, typed} import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.services.Service +import net.psforever.objects.zones.exp import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global @@ -225,8 +226,6 @@ class SessionAvatarHandlers( case AvatarResponse.DestroyDisplay(killer, victim, method, unk) if killer.CharId == avatar.id && killer.Faction != victim.Faction => 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) => // guid = victim // killer = killer @@ -398,71 +397,68 @@ class SessionAvatarHandlers( case AvatarResponse.UpdateKillsDeathsAssists(_, kda) => avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda) - case AvatarResponse.AwardBep(_, bep, expType) => - avatarActor ! AvatarActor.AwardBep(bep, expType) + case AvatarResponse.AwardBep(charId, 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 - val id = player.CharId + val cepConfig = Config.app.game.experience.cep + val charId = player.CharId val squadUI = sessionData.squad.squadUI val participation = continent - .Buildings - .values - .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(buildingId) + .map { building => building.Participation.PlayerContribution() } squadUI - .find { _._1 == id } + .find { _._1 == charId } .collect { case (_, elem) if elem.index == 0 => - //squad leader earns CEP, modified by squad effort - val maxRate: Long = { - val maxCepList = Config.app.game.maximumCepPerSquadSize - val squadSize: Int = { - val squadSizeList: Iterable[Int] = participation - .map { facilityMap => - squadUI.count { case (id, _) => facilityMap.contains(id) } - } - if (squadSizeList.nonEmpty) { - squadSizeList.max - } else { - 0 - } - } + //squad leader earns CEP, modified by squad effort, capped by squad size present during the capture + val squadParticipation = participation match { + case Some(map) => map.filter { case (id, _) => squadUI.contains(id) } + case _ => Map.empty[Long, Float] + } + val maxCepBySquadSize: Long = { + val maxCepList = cepConfig.maximumPerSquadSize + val squadSize: Int = squadParticipation.size maxCepList.lift(squadSize - 1).getOrElse(squadSize * maxCepList.head).toLong } - val groupContribution: Float = { - val eachSquadMemberParticipation: Iterable[Float] = participation.map { facilityMap => - val foundSquadMemberParticipation: Iterable[Float] = squadUI - .keys - .flatMap { facilityMap.get } - if (foundSquadMemberParticipation.nonEmpty) { - foundSquadMemberParticipation.sum / 10f - } else { - 0f - } - } - if (eachSquadMemberParticipation.nonEmpty) { - eachSquadMemberParticipation.max + val groupContribution: Float = squadUI + .map { case (id, _) => (id, squadParticipation.getOrElse(id, 0f) / 10f) } + .values + .max + val modifiedExp: Long = (cep.toFloat * groupContribution).toLong + val cappedModifiedExp: Long = math.min(modifiedExp, maxCepBySquadSize) + val finalExp: Long = if (modifiedExp > cappedModifiedExp) { + val overLimitOverflow = if (cepConfig.squadSizeLimitOverflow == -1) { + cep.toFloat } else { - 0 + cepConfig.squadSizeLimitOverflow.toFloat } + cappedModifiedExp + (overLimitOverflow * cepConfig.squadSizeLimitOverflowMultiplier).toLong + } else { + cappedModifiedExp } - val modifiedExp: Long = math.min((cep.toFloat * groupContribution).toLong, maxRate) - avatarActor ! AvatarActor.AwardCep(modifiedExp) - Some(modifiedExp) + exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, finalExp, expType="cep") + avatarActor ! AvatarActor.AwardCep(finalExp) + Some(finalExp) case _ => //squad member earns BEP based on CEP, modified by personal effort val individualContribution = { val contributionList = for { facilityMap <- participation - if facilityMap.contains(id) - } yield facilityMap(id) + if facilityMap.contains(charId) + } yield facilityMap(charId) if (contributionList.nonEmpty) { contributionList.max } else { @@ -470,26 +466,11 @@ class SessionAvatarHandlers( } } val modifiedExp = (cep * individualContribution).toLong + exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep") avatarActor ! AvatarActor.AwardBep(modifiedExp, ExperienceType.Normal) 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) => sendResponse(msg) @@ -526,6 +507,7 @@ class SessionAvatarHandlers( sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) //player state changes + AvatarActor.updateToolDischargeFor(avatar) player.FreeHand.Equipment.foreach { item => DropEquipmentFromInventory(player)(item) } @@ -540,9 +522,7 @@ class SessionAvatarHandlers( } sessionData.playerActionsToCancel() sessionData.terminals.CancelAllProximityUnits() - sessionData.zoning AvatarActor.savePlayerLocation(player) - sessionData.shooting.reportOngoingShots(sessionData.shooting.reportOngoingShotsToDatabase) sessionData.zoning.spawn.shiftPosition = Some(player.Position) //respawn diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala index 990ab35da..712b71dfe 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -2458,9 +2458,9 @@ class SessionData( src: PlanetSideGameObject with TelepadLike, dest: PlanetSideGameObject with TelepadLike ): Unit = { - val time = System.nanoTime + val time = System.currentTimeMillis() if ( - time - recentTeleportAttempt > (2 seconds).toNanos && router.DeploymentState == DriveState.Deployed && + time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed && internalTelepad.Active && remoteTelepad.Active ) { diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index b88ae7316..3baf77f96 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -7,6 +7,7 @@ import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import akka.pattern.ask import akka.util.Timeout import net.psforever.login.WorldSession +import net.psforever.objects.avatar.scoring.ScoreCard import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.mount.Seat import net.psforever.objects.serverobject.tube.SpawnTube @@ -2342,6 +2343,7 @@ class ZoningOperations( player.avatar = player.avatar.copy(stamina = avatar.maxStamina) avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina) avatarActor ! AvatarActor.ResetImplants() + zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife)) val obj = Player.Respawn(tplayer) DefinitionUtil.applyDefaultLoadout(obj) obj.death_by = tplayer.death_by diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index a5910e792..77cb44d27 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -9847,6 +9847,7 @@ object GlobalDefinitions { resource_silo.Damageable = false resource_silo.Repairable = false 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.Damageable = false @@ -9856,7 +9857,7 @@ object GlobalDefinitions { secondary_capture.Name = "secondary_capture" secondary_capture.Damageable = 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.Damageable = false diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 963b10357..9b7f55181 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -110,6 +110,7 @@ class Player(var avatar: Avatar) Health = Definition.DefaultHealth Armor = MaxArmor Capacitor = 0 + avatar.scorecard.respawn() released = false } isAlive @@ -124,13 +125,16 @@ class Player(var avatar: Avatar) def Revive: Boolean = { Destroyed = false Health = Definition.DefaultHealth + avatar.scorecard.revive() released = false true } def Release: Boolean = { - released = true - backpack = !isAlive + if (!released) { + released = true + backpack = !isAlive + } true } @@ -626,6 +630,7 @@ object Player { if (player.Release) { val obj = new Player(player.avatar) obj.Continent = player.Continent + obj.avatar.scorecard.respawn() obj } else { player diff --git a/src/main/scala/net/psforever/objects/Tool.scala b/src/main/scala/net/psforever/objects/Tool.scala index 17ccd3ce5..0c8b0d286 100644 --- a/src/main/scala/net/psforever/objects/Tool.scala +++ b/src/main/scala/net/psforever/objects/Tool.scala @@ -99,7 +99,7 @@ class Tool(private val toolDef: ToolDefinition) } def Discharge(rounds: Option[Int] = None): Int = { - lastDischarge = System.nanoTime() + lastDischarge = System.currentTimeMillis() Magazine = FireMode.Discharge(this, rounds) } diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala b/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala index 05ce6f846..527332178 100644 --- a/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala +++ b/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala @@ -6,11 +6,14 @@ final case class Life( assists: Seq[Assist], death: Option[Death], equipmentStats: Seq[EquipmentStat], - supportExperience: Long + supportExperience: Long, + prior: Option[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 = { life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned) diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala index 8a0a29407..37e94ed80 100644 --- a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala +++ b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala @@ -18,6 +18,8 @@ class ScoreCard() { def Lives: Seq[Life] = lives + def AllLives: Seq[Life] = curr +: lives + def Kills: Seq[Kill] = lives.flatMap { _.kills } ++ curr.kills def KillStatistics: Map[Int, Statistic] = killStatistics.toMap @@ -39,17 +41,41 @@ class ScoreCard() { ScoreCard.updateStatisticsFor(assistStatistics, wid.equipment, faction) } case d: Death => - val expired = curr - curr = Life() - lives = expired.copy(death = Some(d)) +: lives + curr = curr.copy(death = Some(d)) case value: Long => curr = curr.copy(supportExperience = curr.supportExperience + value) case _ => () } } + + def revive(): Unit = { + curr = Life.revive(curr) + } + + def respawn(): Unit = { + val death = curr + curr = Life() + lives = death +: lives + } } 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 = { updateEquipmentStat(curr, entry, entry.objectId, entry.kills, entry.assists) } diff --git a/src/main/scala/net/psforever/objects/definition/converter/CaptureFlagConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/CaptureFlagConverter.scala index aee98e8ec..bdd7d4906 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/CaptureFlagConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/CaptureFlagConverter.scala @@ -17,7 +17,7 @@ class CaptureFlagConverter extends ObjectCreateConverter[CaptureFlag]() { 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( CaptureFlagData( diff --git a/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala b/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala index 16f12d5b4..a494f2d85 100644 --- a/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala @@ -25,14 +25,14 @@ trait Hackable { def HackedBy_=(agent: Option[Player]): Option[HackInfo] = { (hackedBy, agent) match { 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)) => if (actor.Faction == this.Faction) { //hack cleared hackedBy = None } else if (actor.Faction != info.hackerFaction) { //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) => hackedBy = None diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index 0758f502f..a1cf825be 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -7,11 +7,13 @@ import net.psforever.actors.zone.BuildingActor import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.transfer.TransferBehavior import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.zones import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior, Vehicle} import net.psforever.types.{ExperienceType, PlanetSideEmpire} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.util.Config import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -188,11 +190,16 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) .map { v => (v, v.Owners) } .collect { case (vehicle, Some(owner)) => //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( owner.name, AvatarAction.AwardBep(0, deposit, ExperienceType.Normal) ) + zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit) } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala index eda1b65ad..cb07b97a0 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala @@ -4,12 +4,16 @@ package net.psforever.objects.serverobject.resourcesilo import net.psforever.objects.NtuContainerDefinition import net.psforever.objects.serverobject.structures.AmenityDefinition +import scala.concurrent.duration._ + /** * The definition for any `Resource Silo`. * Object Id 731. */ class ResourceSiloDefinition extends AmenityDefinition(731) with NtuContainerDefinition { + var ChargeTime: FiniteDuration = 0.seconds + Name = "resource_silo" MaxNtuCapacitor = 1000 } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index dbf6336de..df9aa66c6 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -183,8 +183,7 @@ class Building( case Some(obj: CaptureTerminal with Hackable) => obj.HackedBy match { case Some(Hackable.HackInfo(p, _, start, length)) => - val hack_time_remaining_ms = - TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS) + val hack_time_remaining_ms = math.max(0, start + length - System.currentTimeMillis()) (true, p.Faction, hack_time_remaining_ms) case _ => (false, PlanetSideEmpire.NEUTRAL, 0L) diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala index b643c36ab..c64c31156 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala @@ -11,11 +11,12 @@ import scala.collection.mutable trait FacilityHackParticipation extends ParticipationLogic { protected var lastInfoRequest: Long = 0L protected var infoRequestOverTime: Seq[Long] = Seq[Long]() - /* - key: unique player identifier + /** + key: unique player identifier
+ 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 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] = { 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 = { var populationList = list val layer = PlanetSideEmpire.values.map { faction => @@ -56,70 +64,82 @@ trait FacilityHackParticipation extends ParticipationLogic { populationList = everyoneElse (faction, isFaction.size) }.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]( list: Seq[T], newEntry: T, - now: Long = System.currentTimeMillis(), - before: Long + beforeTime: Long ): Seq[T] = { infoRequestOverTime match { - case Nil => - Seq(newEntry) + case Nil => Seq(newEntry) case _ => - val beforeTime = now - before (infoRequestOverTime.indexWhere { _ >= beforeTime } match { - case -1 => - list - case cutOffIndex => - list.drop(cutOffIndex) + case -1 => list + case cutOffIndex => list.drop(cutOffIndex) }) :+ newEntry } } } object FacilityHackParticipation { - private[participation] def calculateExperienceFromKills( + private[participation] def allocateKillsByPlayers( center: Vector3, radius: Float, hackStart: Long, completionTime: Long, opposingFaction: PlanetSideEmpire.Value, contributionVictor: Iterable[(Player, Int, Long)], - contributionOpposingSize: 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) } - val killMapValues = 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 + killMapFunc(contributionVictor) } - private def killsEarnedPerPlayerDuringHack( - centerXY: Vector3, - distanceSq: Float, - start: Long, - end: Long, - faction: PlanetSideEmpire.Value - ) - ( - list: Iterable[(Player, Int, Long)] - ): Iterable[(UniquePlayer, Float, Seq[Kill])] = { + private[participation] def calculateExperienceFromKills( + killMapValues: Iterable[(UniquePlayer, Float, Seq[Kill])], + contributionOpposingSize: Int + ): Long = { + val totalExperienceFromKills = killMapValues + .flatMap { _._3.map { _.experienceEarned } } + .sum + .toFloat + (totalExperienceFromKills * contributionOpposingSize.toFloat * 0.1d).toLong + } + + 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 list.map { case (p, d, _) => val killList = p.avatar.scorecard.Kills.filter { k => 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) } @@ -220,7 +240,8 @@ object FacilityHackParticipation { victorPop <- victorPopulationNumbers opposePop <- opposingPopulationNumbers out = if ( - (opposePop < victorPop && opposePop * healthyPercentage > victorPop) || + (opposePop + victorPop < 8) || + (opposePop < victorPop && opposePop * healthyPercentage > victorPop) || (opposePop > victorPop && victorPop * healthyPercentage > opposePop) ) { 1f //balanced enough population @@ -241,7 +262,7 @@ object FacilityHackParticipation { overwhelmingOddsBonus: Long ): Long = { if (opposingSize * steamrollPercentage < victorSize.toFloat) { - -steamrollBonus * (victorSize - opposingSize) //steamroll by the victor + 0L //steamroll by the victor } else if (victorSize * overwhelmingOddsPercentage <= opposingSize.toFloat) { overwhelmingOddsBonus + opposingSize + victorSize //victory against overwhelming odds } else { diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index 6800ebfbb..4f7639675 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -1,15 +1,17 @@ // Copyright (c) 2023 PSForever package net.psforever.objects.serverobject.structures.participation -import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer} import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector} 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 akka.pattern.ask import akka.util.Timeout +import net.psforever.objects.avatar.scoring.Kill +import net.psforever.objects.zones.exp.ToDatabase + import scala.collection.mutable import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global @@ -49,17 +51,13 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci requestLayers.completeWith(request) request.onComplete { case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) => - hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis(), before = 900000L) + hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis() - 900000L) case _ => requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil))) } requestLayers.future } - private def updateTime(now: Long): Unit = { - infoRequestOverTime = timeSensitiveFilterAndAppend(infoRequestOverTime, now, now, before = 900000L) - } - def RewardFacilityCapture( defenderFaction: PlanetSideEmpire.Value, attackingFaction: PlanetSideEmpire.Value, @@ -70,11 +68,12 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci ): Unit = { val curr = System.currentTimeMillis() val hackStart = curr - completionTime - val (victorFaction, opposingFaction, flagCarrier) = if (!isResecured) { - val carrier = building.GetFlagSocket.flatMap(_.previousFlag).flatMap(_.Carrier) - (attackingFaction, defenderFaction, carrier) + val socketOpt = building.GetFlagSocket + val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) { + val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier) + (attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier) } else { - (defenderFaction, attackingFaction, None) + (defenderFaction, attackingFaction, socketOpt.nonEmpty, None) } val (contributionVictor, contributionOpposing, _) = { 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 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 ... val populationIndices = playerPopulationOverTime.indices 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]] (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 + //Large facility battles should be well-rewarded. val populationModifier = FacilityHackParticipation.populationProgressModifier( opposingPopulationByLayer, { pop => - if (pop > 59) 0.5f - else if (pop > 29) 0.4f - else if (pop > 25) 0.3f - else 0.25f + if (pop > 75) 0.9f + else if (pop > 59) 0.6f + else if (pop > 29) 0.55f + else if (pop > 25) 0.5f + else 0.45f }, - 3 + 4 ) - //3) competition bonus - val competitionBonus: Long = FacilityHackParticipation.competitionBonus( - contributionVictorSize, - contributionOpposingSize, - steamrollPercentage = 1.25f, - steamrollBonus = 5L, - overwhelmingOddsPercentage = 0.5f, - overwhelmingOddsBonus = 100L - ) - //4) competition multiplier + //3) competition multiplier val competitionMultiplier: Float = { val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier( victorPopulationByLayer, opposingPopulationByLayer, - healthyPercentage = 1.25f + healthyPercentage = 1.5f ) //compensate for heat 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)) */ 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 hotSpotLayersOverTime.foldLeft(finalMap) { (map, list) => list.foreach { entry => @@ -153,48 +156,131 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci }.toMap finalMap //explicit for no good reason } - val heatVictorMap = FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values - val heatAgainstMap = FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values - val heatMapModifier = FacilityHackParticipation.heatMapComparison(heatVictorMap, heatAgainstMap) + val heatMapModifier = FacilityHackParticipation.heatMapComparison( + FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values, + FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values + ) heatMapModifier * populationBalanceModifier } - //5) hack time modifier - val overallTimeMultiplier: Float = if (isResecured) { - math.max(0.5f + (hackTime - completionTime) / hackTime, 1f) - } else { - 1f + //4) hack time modifier + //Captured major facilities without a lattice link unit and resecured major facilities with a lattice link unit + // incur the full hack time if the module is not transported to a friendly facility + //Captured major facilities with a lattice link unit and resecure major facilities without a lattice link uit + // 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( - math.max(1L, baseExperienceFromFacilityCapture + competitionBonus) * + math.max(0L, baseExperienceFromFacilityCapture) * populationModifier * competitionMultiplier * overallTimeMultiplier * - Config.app.game.cepRate + Config.app.game.experience.cep.rate + competitionBonus ).toLong - val contributionPerPlayerByTime = playerContribution.collect { - case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 => - (a, 1f) - 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 + //8. reward participants + //Classically, only players in the SOI are rewarded, and the llu runner too + val hackerId = hacker.CharId val events = building.Zone.AvatarEvents - building.PlayersInSOI - .filter { player => - player.Faction == victorFaction && player.CharId != hacker.CharId && !flagCarrier.contains(player) - } + val playersInSoi = building.PlayersInSOI + if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) { + events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep)) + } + playersInSoi + .filter { player => player.Faction == victorFaction && player.CharId != hackerId } .foreach { player => - val contributionMultiplier = contributionPerPlayerByTime.getOrElse(player.CharId, 1f) - events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, (finalCep * contributionMultiplier).toLong)) + val charId = player.CharId + 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 { - 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) + } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala index 410ddf4d4..2e2d459c2 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala @@ -4,7 +4,7 @@ package net.psforever.objects.serverobject.structures.participation import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.sourcing.PlayerSource import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.util.Config final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation { @@ -25,8 +25,6 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack completionTime: Long, isResecured: Boolean ): Unit = { - val curr = System.currentTimeMillis() - val hackStart = curr - completionTime val (victorFaction, opposingFaction) = if (!isResecured) { (attackingFaction, defenderFaction) } else { @@ -40,18 +38,10 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack } val contributionVictorSize = contributionVictor.size 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 ... + import scala.concurrent.duration._ + val curr = System.currentTimeMillis() + val contributionOpposingSize = contributionOpposing.size val populationIndices = playerPopulationOverTime.indices val allFactions = PlanetSideEmpire.values.filterNot { _ == PlanetSideEmpire.NEUTRAL @@ -62,18 +52,74 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack }.toMap[PlanetSideEmpire.Value, Seq[Int]] (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 + //Towers should not be regarded as major battles. + //As the population rises, the rewards decrease (dramatically). val populationModifier = FacilityHackParticipation.populationProgressModifier( - opposingPopulationByLayer, + victorPopulationByLayer, { pop => - if (pop > 59) 0.75f - else if (pop > 29) 0.675f - else if (pop > 25) 0.55f - else 0.5f + if (pop > 80) 0f + else if (pop > 39) (80 - pop).toFloat * 0.01f + else if (pop > 25) 0.5f + else if (pop > 19) 0.55f + else if (pop > 9) 0.6f + else if (pop > 5) 0.75f + else 1f }, 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( contributionVictorSize, contributionOpposingSize, @@ -82,37 +128,29 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack overwhelmingOddsPercentage = 0.5f, overwhelmingOddsBonus = 30L ) - //4) competition multiplier - val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier( - victorPopulationByLayer, - opposingPopulationByLayer, - healthyPercentage = 1.25f - ) - //calculate overall command experience points and the individual player multiplier + //6. calculate overall command experience points val finalCep: Long = math.ceil( - math.max(1L, baseExperienceFromFacilityCapture + competitionBonus) * + baseExperienceFromFacilityCapture * populationModifier * competitionMultiplier * - Config.app.game.cepRate + Config.app.game.experience.cep.rate + competitionBonus ).toLong - val contributionPerPlayerByTime = playerContribution.collect { - case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 => - (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 + //7. reward participants + //Classically, only players in the SOI are rewarded val events = building.Zone.AvatarEvents - building.PlayersInSOI + soiPlayers .filter { player => player.Faction == victorFaction && player.CharId != hacker.CharId } .foreach { player => - val contributionMultiplier = contributionPerPlayerByTime.getOrElse(player.CharId, 1f) - events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, (finalCep * contributionMultiplier).toLong)) + val charId = player.CharId + 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)) } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index c3ab7e93f..ff61eee36 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -101,7 +101,7 @@ class FacilityTurretControl(turret: FacilityTurret) turret.ControlledWeapon(wepNumber = 1).foreach { case weapon: Tool => // 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 val seat = turret.Seat(0).get seat.occupant match { diff --git a/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala index b6e40a1b5..97d6b8576 100644 --- a/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala @@ -187,8 +187,10 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior { val chargeToDeposit = if (min == 0) { transferTarget match { 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(silo.MaxNtuCapacitor / 105, chargeable.NtuCapacitor), max) + scala.math.min( + scala.math.min(silo.MaxNtuCapacitor / silo.Definition.ChargeTime.toMillis.toFloat, chargeable.NtuCapacitor), + max + ) case _ => 0 } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index 52e8146e8..18a3d9797 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -893,7 +893,7 @@ object VehicleControl { /** * 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 * @return `true`, if the shield charge would be blocked; * `false`, otherwise diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala index cd5177b2b..eebc44eca 100644 --- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala @@ -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.projectile.ProjectileReason import net.psforever.types.{ExoSuitType, ImplantType, TransactionType} +import net.psforever.util.Config import scala.collection.mutable @@ -327,7 +328,7 @@ trait InGameHistory { } 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] = { diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala index ada240c1b..f966318ba 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala @@ -78,23 +78,23 @@ object KillAssists { lastDamage: Option[DamageResult], history: Iterable[InGameActivity], ): Seq[(PlayerSource, KDAStat)] = { - val shortHistory = limitHistoryToThisLife(history.toList) - determineKiller(lastDamage, shortHistory) match { + val truncatedHistory = limitHistoryToThisLife(history.toList) + determineKiller(lastDamage, truncatedHistory) match { case Some((result, killer: PlayerSource)) => - val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer)) - val fullBep = calculateExperience(killer, victim, shortHistory) + val assists = collectKillAssistsForPlayer(victim, truncatedHistory, Some(killer)) + val fullBep = calculateExperience(killer, victim, truncatedHistory) val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map { case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong)) }.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 _ => - val assists = collectKillAssistsForPlayer(victim, shortHistory, None) - val fullBep = Support.baseExperience(victim, shortHistory) + val assists = collectKillAssistsForPlayer(victim, truncatedHistory, None) + val fullBep = Support.baseExperience(victim, truncatedHistory) val hitSquad = assists.map { case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong)) }.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 } } diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala index c315429f6..93293aaa3 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala @@ -10,6 +10,7 @@ import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.{PlanetSideEmpire, Vector3} +import net.psforever.util.Config import scala.collection.mutable @@ -49,7 +50,7 @@ object KillContributions { multivehicle_rearm_terminal, lodestar_repair_terminal ).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 */ private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats] @@ -119,21 +120,20 @@ object KillContributions { ): Iterable[(Long, ContributionStatsOutput)] = { 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; 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 */ - //divide by applicable time periods (long=10minutes, short=5minutes) - val (contributions, (shortHistory, longHistory)) = { + val (contributions, (longHistory, shortHistory)) = { 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] } ( contrib .collect { case Contribution(unique, entries) => (unique, entries) } .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 @@ -195,7 +195,7 @@ object KillContributions { * @return the potentially truncated history */ 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] ): mutable.LongMap[ContributionStats] = { contributeWithRevivalActivity(history, existingParticipants) - contributeWithTerminalActivity(faction, history, contributions, excludedTargets, existingParticipants) - contributeWithVehicleTransportActivity(history, faction, contributions, excludedTargets, existingParticipants) - contributeWithVehicleCargoTransportActivity(history, faction, contributions, excludedTargets, existingParticipants) + contributeWithTerminalActivity(history, faction, contributions, excludedTargets, existingParticipants) + contributeWithVehicleTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants) + contributeWithVehicleCargoTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants) contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants) existingParticipants.remove(0) existingParticipants @@ -314,13 +314,17 @@ object KillContributions { excludedTargets.addOne(owner) excludedTargets.addOne(attacker.unique) val time = kill.time.toDate.getTime + val weaponStat = Support.calculateSupportExperience( + event = "mounted-kill", + WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 1f) + ) combineStatsInto( out, ( owner.charId, ContributionStats( PlayerSource(owner, mount.Position), - Seq(WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 10f)), + Seq(weaponStat), 1, 1, 1, @@ -332,12 +336,12 @@ object KillContributions { } combineStatsInto( 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) => combineStatsInto( 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.
* 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 faction empire to target * @param contributions mapping between external entities @@ -356,6 +361,7 @@ object KillContributions { * @see `extractContributionsForMachineByTarget` */ private def contributeWithVehicleTransportActivity( + kill: Kill, history: List[InGameActivity], faction: PlanetSideEmpire.Value, contributions: Map[SourceUniqueness, List[InGameActivity]], @@ -385,11 +391,19 @@ object KillContributions { } } || { 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 - timeSpent >= 210000 /* 3:30 */ || - (sameZone && (distanceMoved > 160000f || distanceMoved > 10000f && timeSpent >= 60000)) | - (!sameZone && (distanceMoved > 10000f || timeSpent >= 120000)) + distanceMoved < 5625f /* 75m */ && + (timeSpent >= 210000L /* 3:30 */ || + (sameZone && (distanceTransported > 160000f /* 400m */ || + distanceTransported > 10000f /* 100m */ && timeSpent >= 60000L /* 1:00m */)) || + (!sameZone && (distanceTransported > 10000f /* 100m */ || timeSpent >= 120000L /* 2:00 */ ))) }) => out } @@ -398,18 +412,23 @@ object KillContributions { .groupBy { _.vehicle } .collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty => val promotedOwner = PlayerSource(mount.owner.get, mount.Position) - val equipmentUseContext = mount.Definition match { + val (equipmentUseContext, equipmentUseEvent) = mount.Definition match { case v @ GlobalDefinitions.router => - RouterKillAssist(v.ObjectId) + (RouterKillAssist(v.ObjectId), "router") 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( out, ( promotedOwner.CharId, - ContributionStats(promotedOwner, statContext, 1, 1, 1, statContext.head.time) + ContributionStats(promotedOwner, Seq(weaponStat), size, size, size, time) ) ) contributions.get(mount.unique).collect { @@ -417,13 +436,13 @@ object KillContributions { val mountHistory = dismountsFromVehicle .flatMap { event => 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) } .distinctBy(_.time) combineStatsInto( 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.
* na + * @param kill the in-game event that maintains information about the other player's death * @param faction empire to target * @param contributions mapping between external entities * the target has interacted with in the form of in-game activity @@ -442,6 +462,7 @@ object KillContributions { * @see `extractContributionsForMachineByTarget` */ private def contributeWithVehicleCargoTransportActivity( + kill: Kill, history: List[InGameActivity], faction: PlanetSideEmpire.Value, contributions: Map[SourceUniqueness, List[InGameActivity]], @@ -463,9 +484,16 @@ object KillContributions { if in.vehicle.unique == out.vehicle.unique && out.vehicle.Faction == out.cargo.Faction && (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 - timeSpent >= 210000 /* 3:30 */ || distanceMoved > 640000f + distanceMoved < 5625f /* 75m */ && + (timeSpent >= 210000 /* 3:30 */ || distanceTransported > 360000f /* 600m */) }) => out } @@ -478,7 +506,13 @@ object KillContributions { dismountsFromVehicle .groupBy(_.vehicle) .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) => combineStatsInto( @@ -493,13 +527,13 @@ object KillContributions { val mountHistory = dismountsFromVehicle .flatMap { event => 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) } .distinctBy(_.time) combineStatsInto( out, - extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets) + extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair") ) } contributions.get(vehicle.unique).collect { @@ -507,13 +541,13 @@ object KillContributions { val carrierHistory = dismountsFromVehicle .flatMap { event => 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) } .distinctBy(_.time) combineStatsInto( out, - extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets) + extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets, eventOutputType="support-repair") ) } } @@ -542,104 +576,120 @@ object KillContributions { * @see `TerminalUsedActivity` */ private def contributeWithTerminalActivity( - faction: PlanetSideEmpire.Value, history: List[InGameActivity], + faction: PlanetSideEmpire.Value, contributions: Map[SourceUniqueness, List[InGameActivity]], excludedTargets: mutable.ListBuffer[SourceUniqueness], out: mutable.LongMap[ContributionStats] ): Unit = { - val data = history + history .collect { - case h: HealFromTerminal => (h.term, (h, h.term.hacked)) - case r: RepairFromTerminal => (r.term, (r, r.term.hacked)) - case t: TerminalUsedActivity => (t.terminal, (t, t.terminal.hacked)) + case h: HealFromTerminal => (h.term, h) + case r: RepairFromTerminal => (r.term, r) + case t: TerminalUsedActivity => (t.terminal, t) } .groupBy(_._1.unique) - .map { case (_, list) => (list.head._1, list.map { _._2 }) } - data.flatMap { - case (terminal, events) => - val (activity, hackState) = events.unzip - val terminalFaction = terminal.Faction - if (terminalFaction != faction && hackState.exists { _.nonEmpty }) { - /* - if the terminal has been hacked, - and the original terminal does not align with our own faction, - then the support must be reported as a hack; - if we are the same faction as the terminal, then the hacked condition is irrelevant - */ - val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, terminal.Definition.ObjectId) - hackState - .groupBy(_.get.player) - .collect { - case (player, _) if player.Faction == faction => //only reward allied hacking - val time = activity.maxBy(_.time).time + .map { + case (_, events1) => + val (termThings1, _) = events1.unzip + val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, termThings1.head.Definition.ObjectId) + if (termThings1.exists(t => t.Faction != faction && t.hacked.nonEmpty)) { + /* + if the terminal has been hacked, + and the original terminal does not align with our own faction, + then the support must be reported as a hack; + if we are the same faction as the terminal, then the hacked condition is irrelevant + */ + events1 + .collect { case out @ (t, _) if t.hacked.nonEmpty => out } + .groupBy { case (t, _) => t.hacked.get.player.unique } + .foreach { case (_, events2) => + val (termThings2, events3) = events2.unzip + val hacker = termThings2.head.hacked.get.player + val size = events3.size + val time = events3.maxBy(_.time).time + val weaponStats = Support.calculateSupportExperience( + event = "hack", + WeaponStats(hackContext, size, size, time, 1f) + ) combineStatsInto( out, ( - player.CharId, + hacker.CharId, ContributionStats( - player, - Seq(WeaponStats(hackContext, 0, 1, time, 10f)), - 0, - 0, - 1, + hacker, + Seq(weaponStats), + size, + size, + size, 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 => - combineStatsInto(out, extractContributionsForMachineByTarget(terminal, faction, eventTime, startTime, contributions, excludedTargets)) - (NoUse(), None) //general terminal use - case _ => - (NoUse(), None) - } - ownerOpt.collect { owner => - val time = activity.maxBy(_.time).time - combineStatsInto( - out, - ( - owner.charId, - ContributionStats( - PlayerSource(owner, terminal.installation.Position), - Seq(WeaponStats(equipmentUseContext, 0, 1, time, 10f)), - 0, - 0, - 1, - time + } else if (termThings1.exists(_.Faction == faction)) { + //faction-aligned terminal + val (_, events2) = events1.unzip + val eventTime = events2.maxBy(_.time).time + val startTime = events2.minBy(_.time).time - Config.app.game.experience.longContributionTime + val termThingsHead = termThings1.head + val (equipmentUseContext, equipmentUseEvent, installationEvent, target) = termThingsHead.installation match { + case v: VehicleSource => + termThingsHead.Definition match { + case GlobalDefinitions.order_terminala => + (AmsResupplyKillAssist(GlobalDefinitions.order_terminala.ObjectId), "ams-resupply", "support-repair", Some(v)) + case GlobalDefinitions.order_terminalb => + (AmsResupplyKillAssist(GlobalDefinitions.order_terminalb.ObjectId), "ams-resupply", "support-repair", Some(v)) + case GlobalDefinitions.lodestar_repair_terminal => + (RepairKillAssist(GlobalDefinitions.lodestar_repair_terminal.ObjectId, v.Definition.ObjectId), "lodestar-repair", "support-repair", Some(v)) + case GlobalDefinitions.bfr_rearm_terminal => + (LodestarRearmKillAssist(GlobalDefinitions.bfr_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v)) + case GlobalDefinitions.multivehicle_rearm_terminal => + (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 - } else { - Nil - } - } + None + } } /** @@ -671,7 +721,12 @@ object KillContributions { id, ContributionStats( 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, @@ -700,10 +755,11 @@ object KillContributions { faction: PlanetSideEmpire.Value, time: Long, contributions: Map[SourceUniqueness, List[InGameActivity]], - excludedTargets: mutable.ListBuffer[SourceUniqueness] + excludedTargets: mutable.ListBuffer[SourceUniqueness], + eventOutputType: String ): mutable.LongMap[ContributionStats] = { - val start: Long = time - 600000L - extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets) + val start: Long = time - Config.app.game.experience.longContributionTime + extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets, eventOutputType) } /** @@ -726,11 +782,12 @@ object KillContributions { eventTime: Long, startTime: Long, contributions: Map[SourceUniqueness, List[InGameActivity]], - excludedTargets: mutable.ListBuffer[SourceUniqueness] + excludedTargets: mutable.ListBuffer[SourceUniqueness], + eventOutputType: String ): mutable.LongMap[ContributionStats] = { val unique = target.unique 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, history: List[InGameActivity], contributions: Map[SourceUniqueness, List[InGameActivity]], - excludedTargets: mutable.ListBuffer[SourceUniqueness] + excludedTargets: mutable.ListBuffer[SourceUniqueness], + eventOutputType: String ): mutable.LongMap[ContributionStats] = { val unique = target.unique if (!excludedTargets.contains(unique) && history.nonEmpty) { excludedTargets.addOne(unique) - val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, excludedTargets) + val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, eventOutputType, excludedTargets) process.submit(history) cullContributorImplements(process.output()) } else { diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala index 08fa1dbf4..98a2dfc11 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala @@ -4,6 +4,7 @@ package net.psforever.objects.zones.exp import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.vital.{InGameActivity, ReconstructionActivity, RepairFromExoSuitChange, SpawningActivity} import net.psforever.types.{ExoSuitType, PlanetSideEmpire} +import net.psforever.util.Config import scala.collection.mutable @@ -11,6 +12,8 @@ import scala.collection.mutable * Functions to assist experience calculation and history manipulation and analysis. */ object Support { + private val sep = Config.app.game.experience.sep + /** * Calculate a base experience value to consider additional reasons for points. * @param victim player to which a final interaction has reduced health to zero @@ -27,18 +30,19 @@ object Support { case _ => 0L } val base = if (Support.wasEverAMax(victim, history)) { - 250L - } else if (victim.Seated || victim.progress.kills.nonEmpty) { - 100L + Config.app.game.experience.bep.base.asMax + } else if (victim.progress.kills.nonEmpty) { + Config.app.game.experience.bep.base.withKills + } else if (victim.Seated) { + Config.app.game.experience.bep.base.asMounted } else if (lifespan > 15000L) { - 50L + Config.app.game.experience.bep.base.mature } else { 1L } if (base > 1) { //black ops modifier - //TODO x10 - base + base * Config.app.game.experience.bep.base.bopsMultiplier } else { base } @@ -187,4 +191,49 @@ object Support { case _ => false } } + + /** + * Take a weapon statistics entry and calculate the support experience value resulting from this support event. + * The complete formula is:

+ * `base + shots-multplier * ln(shots^exp + 2) + amount-multiplier * amount`

+ * ... where the middle field can be truncated into:

+ * `shots-multplier * shots`

+ * ... 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) + } } diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala index 62b76a04e..fc9801c2b 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala @@ -2,7 +2,6 @@ package net.psforever.objects.zones.exp import scala.concurrent.ExecutionContext.Implicits.global - import net.psforever.objects.avatar.scoring.EquipmentStat import net.psforever.objects.serverobject.hackable.Hackable.HackInfo import net.psforever.objects.sourcing.VehicleSource @@ -11,8 +10,6 @@ import net.psforever.types.Vector3 import net.psforever.util.Database.ctx import net.psforever.util.Database.ctx._ -import scala.util.Success - object ToDatabase { /** * Insert an entry into the database's `killactivity` table. @@ -57,7 +54,7 @@ object ToDatabase { position: Vector3, exp: Long ): Unit = { - ctx.run(query[persistence.Assistactivity] + ctx.run(query[persistence.Killactivity] .insert( _.killerId -> lift(avatarId), _.victimId -> lift(victimId), @@ -115,6 +112,10 @@ object ToDatabase { _.assists -> lift(0), _.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) + ) + ) + } } diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala index 8e0685f06..59b9a2089 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala @@ -3,7 +3,7 @@ package net.psforever.objects.zones.exp.rec import net.psforever.objects.sourcing.SourceUniqueness 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 scala.collection.mutable @@ -58,23 +58,32 @@ class CombinedHealthAndArmorContributionProcess( } .map { case (id, entry) => + var totalShots: Int = 0 + var totalAmount: Int = 0 + var mostRecentTime: Long = 0 val groupedWeapons = entry.weapons .groupBy(_.equipment) .map { - case (weaponId, weaponEntries) => - val specificEntries = weaponEntries.filter(_.equipment == weaponId) + case (weaponContext, weaponEntries) => + val specificEntries = weaponEntries.filter(_.equipment == weaponContext) val amount = specificEntries.foldLeft(0)(_ + _.amount) + totalAmount = totalAmount + amount 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 - (id, ContributionStats( - player = entry.player, + (id, entry.copy( weapons = groupedWeapons, - amount = entry.amount + entry.amount, - total = entry.total + entry.total, - shots = groupedWeapons.foldLeft(0)(_ + _.shots), - time = groupedWeapons.maxBy(_.time).time + amount = totalAmount, + total = math.max(entry.total, totalAmount), + shots = totalShots, + time = mostRecentTime )) } } diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala index ce0404888..c1544fa46 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala @@ -3,15 +3,15 @@ package net.psforever.objects.zones.exp.rec import net.psforever.objects.sourcing.SourceUniqueness 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 scala.collection.mutable -//noinspection ScalaUnusedSymbol class MachineRecoveryExperienceContributionProcess( private val faction : PlanetSideEmpire.Value, private val contributions: Map[SourceUniqueness, List[InGameActivity]], + eventOutputType: String, private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer() ) extends RecoveryExperienceContributionProcess(faction, contributions) { def submit(history: List[InGameActivity]): Unit = { @@ -65,11 +65,9 @@ class MachineRecoveryExperienceContributionProcess( .map { case (wrapper, entries) => val size = entries.size val newTime = entries.maxBy(_.time).time - entries.head.copy( - shots = size, - amount = entries.foldLeft(0)(_ + _.amount), - contributions = (10 + size).toFloat, - time = newTime + Support.calculateSupportExperience( + eventOutputType, + WeaponStats(wrapper, size, entries.foldLeft(0)(_ + _.amount), newTime, 1f) ) } .toSeq diff --git a/src/main/scala/net/psforever/persistence/Assistactivity.scala b/src/main/scala/net/psforever/persistence/Assistactivity.scala deleted file mode 100644 index 9d59ad53c..000000000 --- a/src/main/scala/net/psforever/persistence/Assistactivity.scala +++ /dev/null @@ -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() - ) diff --git a/src/main/scala/net/psforever/persistence/KdaExp.scala b/src/main/scala/net/psforever/persistence/KdaExp.scala new file mode 100644 index 000000000..66fcc0834 --- /dev/null +++ b/src/main/scala/net/psforever/persistence/KdaExp.scala @@ -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 + ) diff --git a/src/main/scala/net/psforever/persistence/Killactivity.scala b/src/main/scala/net/psforever/persistence/Killactivity.scala deleted file mode 100644 index 2eb6a1f7e..000000000 --- a/src/main/scala/net/psforever/persistence/Killactivity.scala +++ /dev/null @@ -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() - ) diff --git a/src/main/scala/net/psforever/persistence/Machinedestroyed.scala b/src/main/scala/net/psforever/persistence/Machinedestroyed.scala deleted file mode 100644 index c25e4eac1..000000000 --- a/src/main/scala/net/psforever/persistence/Machinedestroyed.scala +++ /dev/null @@ -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() - ) diff --git a/src/main/scala/net/psforever/persistence/Supportactivity.scala b/src/main/scala/net/psforever/persistence/Supportactivity.scala deleted file mode 100644 index d131adaec..000000000 --- a/src/main/scala/net/psforever/persistence/Supportactivity.scala +++ /dev/null @@ -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() - ) diff --git a/src/main/scala/net/psforever/persistence/Weaponstatsession.scala b/src/main/scala/net/psforever/persistence/Weaponstatsession.scala deleted file mode 100644 index 7aca2bf82..000000000 --- a/src/main/scala/net/psforever/persistence/Weaponstatsession.scala +++ /dev/null @@ -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 - ) diff --git a/src/main/scala/net/psforever/services/RemoverActor.scala b/src/main/scala/net/psforever/services/RemoverActor.scala index 0348a3c1b..d6118dccc 100644 --- a/src/main/scala/net/psforever/services/RemoverActor.scala +++ b/src/main/scala/net/psforever/services/RemoverActor.scala @@ -82,7 +82,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] { entryManagementBehaviors .orElse { 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))) { InitialJob(entry) if (entry.duration == 0) { @@ -120,7 +120,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] { case RemoverActor.StartDelete() => firstTask.cancel() secondTask.cancel() - val now: Long = System.nanoTime + val now: Long = System.currentTimeMillis() val (in, out) = firstHeap.partition(entry => { 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 */ 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. * 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); - * defaults to the current time (in nanoseconds) + * @param now the time (in milliseconds); + * defaults to the current time (in milliseconds) */ - def RetimeFirstTask(now: Long = System.nanoTime): Unit = { + def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = { firstTask.cancel() 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 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. * 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 /** * Default time for entries waiting in the second list. * 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 @@ -322,7 +322,7 @@ object RemoverActor extends SupportActorCaseConversions { * Internally, all entries have a "time created" field. * @param _obj the target * @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) extends SupportActor.Entry(_obj, _zone, _duration) @@ -332,7 +332,7 @@ object RemoverActor extends SupportActorCaseConversions { * @see `FirstStandardDuration` * @param obj the target * @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 */ case class AddTask(obj: PlanetSideGameObject, zone: Zone, duration: Option[FiniteDuration] = None) diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index 53c50321d..8ee7edb52 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -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 _ => () } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index d66925062..33d5b894f 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -158,6 +158,7 @@ object AvatarAction { 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 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 PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index af730abcd..05ba1a1d5 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -129,4 +129,5 @@ object AvatarResponse { 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 AwardCep(charId: Long, bep: Long) extends Response + final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response } diff --git a/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala b/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala index 3d25dcb41..115924267 100644 --- a/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala +++ b/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala @@ -49,8 +49,7 @@ class DoorCloseActor() extends Actor { }) if (openDoors.nonEmpty) { - val short_timeout: FiniteDuration = - math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)) nanoseconds + val short_timeout: FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)).milliseconds import scala.concurrent.ExecutionContext.Implicits.global 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`, * processing in order is always correct. * @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` * @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, * and a `List` of elements that still satisfy the time limit. * @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; * defaults to 0 * @return the index where division will occur diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index 500fc63da..5f259aee0 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -17,7 +17,6 @@ import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} -import java.util.concurrent.TimeUnit import scala.concurrent.duration.{FiniteDuration, _} import scala.util.Random @@ -38,7 +37,7 @@ class HackCaptureActor extends Actor { val duration = target.Definition.FacilityHackTime target.HackedBy match { case Some(hackInfo) => - target.HackedBy = hackInfo.Duration(duration.toNanos) + target.HackedBy = hackInfo.Duration(duration.toMillis) case None => log.error(s"Initial $target hack information is missing") } @@ -57,8 +56,8 @@ class HackCaptureActor extends Actor { case HackCaptureActor.ProcessCompleteHacks() => log.trace("Processing complete hacks") clearTrigger.cancel() - val now: Long = System.nanoTime - val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toNanos) + val now: Long = System.currentTimeMillis() + val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toMillis) hackedObjects = stillHacked finishedHacks.foreach { entry => val terminal = entry.target @@ -213,9 +212,8 @@ class HackCaptureActor extends Actor { private def RestartTimer(): Unit = { if (hackedObjects.nonEmpty) { - val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.nanoTime())) - val short_timeout: FiniteDuration = - math.max(1, hackEntry.duration.toNanos - (System.nanoTime - hackEntry.hack_timestamp)).nanoseconds + val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.currentTimeMillis())) + val short_timeout: FiniteDuration = math.max(1, hackEntry.duration.toMillis - (System.currentTimeMillis() - hackEntry.hack_timestamp)).milliseconds log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds") import scala.concurrent.ExecutionContext.Implicits.global clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks()) @@ -229,7 +227,7 @@ object HackCaptureActor { zone: Zone, unk1: Long, unk2: Long, - startTime: Long = System.nanoTime() + startTime: Long = System.currentTimeMillis() ) final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource) @@ -256,8 +254,7 @@ object HackCaptureActor { 17039360L case Some(Hackable.HackInfo(p, _, start, length)) => // See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated - val hackTimeRemainingMS = - TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS) + val hackTimeRemainingMS = math.max(0, start + length - System.currentTimeMillis()) val startNum = p.Faction match { case PlanetSideEmpire.TR => 0x10000 case PlanetSideEmpire.NC => 0x20000 @@ -273,8 +270,8 @@ object HackCaptureActor { entry1: HackCaptureActor.HackEntry, entry2: HackCaptureActor.HackEntry ): HackCaptureActor.HackEntry = { - val entry1TimeLeft = entry1.duration.toNanos - (now - entry1.hack_timestamp) - val entry2TimeLeft = entry2.duration.toNanos - (now - entry2.hack_timestamp) + val entry1TimeLeft = entry1.duration.toMillis - (now - entry1.hack_timestamp) + val entry2TimeLeft = entry2.duration.toMillis - (now - entry2.hack_timestamp) if (entry1TimeLeft < entry2TimeLeft) { entry1 } else { diff --git a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala index 2e3599fec..c6f8e8bb4 100644 --- a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala @@ -29,15 +29,15 @@ class HackClearActor() extends Actor { def receive: Receive = { case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) => - val durationNanos = TimeUnit.NANOSECONDS.convert(duration, TimeUnit.SECONDS) - hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationNanos) + val durationMillis = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS) + 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 RestartTimer() case HackClearActor.TryClearHacks() => 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 val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now) hackedObjects = stillHackedObjects @@ -75,12 +75,12 @@ class HackClearActor() extends Actor { private def RestartTimer(): Unit = { if (hackedObjects.nonEmpty) { - val now = System.nanoTime() + val now = System.currentTimeMillis() val (_/*unhackObjects*/, stillHackedObjects) = PartitionEntries(hackedObjects, now) stillHackedObjects.headOption match { 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( 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`, * processing in order is always correct. * @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` * @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, * and a `List` of elements that still satisfy the time limit. * @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; * defaults to 0 * @return the index where division will occur @@ -158,7 +158,7 @@ object HackClearActor { unk1: Long, unk2: Long, duration: Int, - time: Long = System.nanoTime() + time: Long = System.currentTimeMillis() ) /** @@ -185,7 +185,7 @@ object HackClearActor { * @param target the server object * @param zone the zone in which the object resides * @param time when the object was hacked - * @param duration The hack duration in nanoseconds + * @param duration The hack duration in milliseconds * @see `ObjectIsHacked` */ private final case class HackEntry( diff --git a/src/main/scala/net/psforever/services/support/SupportActor.scala b/src/main/scala/net/psforever/services/support/SupportActor.scala index dd3076e4f..bb52e1f0b 100644 --- a/src/main/scala/net/psforever/services/support/SupportActor.scala +++ b/src/main/scala/net/psforever/services/support/SupportActor.scala @@ -96,7 +96,7 @@ abstract class SupportActor[A <: SupportActor.Entry] extends Actor { object SupportActor { class Entry(val obj: PlanetSideGameObject, val zone: Zone, val duration: Long) { - val time: Long = System.nanoTime + val time: Long = System.currentTimeMillis() } /** diff --git a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala index fa93a1292..eb064d72b 100644 --- a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala +++ b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala @@ -50,8 +50,8 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { entryManagementBehaviors .orElse { case TurretUpgrader.AddTask(turret, zone, upgrade, duration) => - val lengthOfTime = duration.getOrElse(TurretUpgrader.StandardUpgradeLifetime).toNanos - if (lengthOfTime > (1 second).toNanos) { //don't even bother if it's too short; it'll revert near instantly + val lengthOfTime = duration.getOrElse(TurretUpgrader.StandardUpgradeLifetime).toMillis + 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) UpgradeTurretAmmo(CreateEntry(turret, zone, upgrade, lengthOfTime)) if (list.isEmpty) { @@ -73,7 +73,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { case TurretUpgrader.Downgrade() => task.cancel() - val now: Long = System.nanoTime + val now: Long = System.currentTimeMillis() val (in, out) = list.partition(entry => { now - entry.time >= entry.duration @@ -85,10 +85,10 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { case _ => ; } - def RetimeFirstTask(now: Long = System.nanoTime): Unit = { + def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = { task.cancel() 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 task = context.system.scheduler.scheduleOnce(short_timeout, self, TurretUpgrader.Downgrade()) } @@ -249,7 +249,7 @@ object TurretUpgrader extends SupportActorCaseConversions { * @param _obj the target * @param _zone the zone in which this target is registered * @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) extends SupportActor.Entry(_obj, _zone, _duration) diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index 6c3b8e9de..cdde9a011 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -150,9 +150,6 @@ case class GameConfig( instantAction: InstantActionConfig, amenityAutorepairRate: Float, amenityAutorepairDrainRate: Float, - bepRate: Double, - cepRate: Double, - maximumCepPerSquadSize: Seq[Int], newAvatar: NewAvatar, hart: HartConfig, sharedMaxCooldown: Boolean, @@ -162,7 +159,8 @@ case class GameConfig( cavernRotation: CavernRotationConfig, savedMsg: SavedMessageEvents, playerDraw: PlayerStateDrawSettings, - doorsCanBeOpenedByMedAppFromThisDistance: Float + doorsCanBeOpenedByMedAppFromThisDistance: Float, + experience: Experience ) case class InstantActionConfig( @@ -238,3 +236,51 @@ case class PlayerStateDrawSettings( assert(ranges.nonEmpty) 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 +) diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index 9266d0c79..df5833690 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -624,7 +624,7 @@ class PlayerControlDeathStandingTest extends ActorTest { // override def AvatarEvents = avatarProbe.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.Spawn()