Merged in M4SS/holocore/feature/gcw (pull request #545)

Galactic Civil War
This commit is contained in:
Mads Boddum
2018-02-20 19:00:58 +00:00
committed by Obique PSWG
7 changed files with 605 additions and 6 deletions
@@ -0,0 +1,59 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create an emulator which will provide a server for players to *
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
* This file is part of Holocore. *
* *
* --------------------------------------------------------------------------------*
* *
* Holocore is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* Holocore is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
***********************************************************************************/
package com.projectswg.holocore.intents;
import com.projectswg.common.control.Intent;
import com.projectswg.holocore.resources.objects.player.PlayerObject;
import javax.annotation.Nonnull;
public class CivilWarPointIntent extends Intent {
private final PlayerObject receiver;
private final int points;
@Nonnull
private CivilWarPointIntent(PlayerObject receiver, int points) {
this.receiver = receiver;
this.points = points;
}
@Nonnull
public PlayerObject getReceiver() {
return receiver;
}
public int getPoints() {
return points;
}
@Nonnull
public static void broadcast(PlayerObject receiver, int points) {
new CivilWarPointIntent(receiver, points).broadcast();
}
}
@@ -32,6 +32,8 @@ import com.projectswg.common.data.location.Terrain;
import com.projectswg.common.data.sui.SuiEvent;
import com.projectswg.common.debug.Log;
import com.projectswg.holocore.intents.CivilWarPointIntent;
import com.projectswg.holocore.resources.objects.player.PlayerObject;
import com.projectswg.holocore.scripts.commands.admin.qatool.QaToolDetails;
import com.projectswg.holocore.intents.chat.SystemMessageIntent;
import com.projectswg.holocore.intents.experience.ExperienceIntent;
@@ -102,6 +104,9 @@ public class QaToolCmdCallback implements ICmdCallback {
else
SystemMessageIntent.broadcastPersonal(player, "QATool XP: Expected format: /qatool xp <xpType> <xpGained>");
break;
case "gcw":
grantGcw(player, command[1]);
break;
default:
displayMainWindow(player);
break;
@@ -226,6 +231,19 @@ public class QaToolCmdCallback implements ICmdCallback {
}
}
private void grantGcw(Player player, String pointsArg) {
PlayerObject receiver = player.getCreatureObject().getPlayerObject();
try {
int points = Integer.parseInt(pointsArg);
CivilWarPointIntent.broadcast(receiver, points);
Log.i("GCW command: %s gave themselves %d GCW points", player.getUsername(), points);
} catch (NumberFormatException e) {
SystemMessageIntent.broadcastPersonal(player, String.format("GCW command: %s is not a number", pointsArg));
}
}
/* Utility Methods */
private void createMessageBox(Player player, String title, String prompt) {
@@ -0,0 +1,406 @@
/************************************************************************************
* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create an emulator which will provide a server for players to *
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
* This file is part of Holocore. *
* *
* -------------------------------------------------------------------------------- *
* *
* Holocore is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* Holocore is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.holocore.services.faction;
import com.projectswg.common.concurrency.PswgScheduledThreadPool;
import com.projectswg.common.control.Service;
import com.projectswg.common.data.encodables.oob.ProsePackage;
import com.projectswg.common.data.encodables.oob.StringId;
import com.projectswg.common.data.encodables.tangible.PvpFaction;
import com.projectswg.common.data.encodables.tangible.PvpStatus;
import com.projectswg.common.debug.Log;
import com.projectswg.common.network.packets.swg.zone.PlayClientEffectObjectMessage;
import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage;
import com.projectswg.holocore.intents.CivilWarPointIntent;
import com.projectswg.holocore.intents.chat.SystemMessageIntent;
import com.projectswg.holocore.intents.combat.CreatureKilledIntent;
import com.projectswg.holocore.intents.object.DestroyObjectIntent;
import com.projectswg.holocore.intents.object.ObjectCreatedIntent;
import com.projectswg.holocore.resources.objects.SWGObject;
import com.projectswg.holocore.resources.objects.creature.CreatureDifficulty;
import com.projectswg.holocore.resources.objects.creature.CreatureObject;
import com.projectswg.holocore.resources.objects.player.PlayerObject;
import com.projectswg.holocore.resources.server_info.SdbLoader;
import com.projectswg.holocore.resources.server_info.StandardLog;
import java.io.File;
import java.io.IOException;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
final class CivilWarService extends Service {
private static final int IMPERIAL_INDEX = 0;
private static final int REBEL_INDEX = 1;
private final Map<Integer, String[]> rankAbilities;
private final Set<PlayerObject> playerObjects;
private final PswgScheduledThreadPool threadPool;
private final DayOfWeek updateWeekDay;
private final LocalTime updateTime;
private final ZoneOffset updateOffset;
private int rankEpoch;
CivilWarService() {
rankAbilities = new HashMap<>();
playerObjects = ConcurrentHashMap.newKeySet();
threadPool = new PswgScheduledThreadPool(1, "civil-war-service");
// Rank update time is the night between Thursday and Friday at 00:00 UTC
updateWeekDay = DayOfWeek.FRIDAY;
updateTime = LocalTime.MIDNIGHT;
updateOffset = ZoneOffset.UTC;
loadRankAbilities();
registerForIntent(CivilWarPointIntent.class, this::handleCivilWarPointIntent);
registerForIntent(CreatureKilledIntent.class, this::handleCreatureKilledIntent);
registerForIntent(DestroyObjectIntent.class, this::handleDestroyObjectIntent);
registerForIntent(ObjectCreatedIntent.class, this::handleObjectCreatedIntent);
}
@Override
public boolean initialize() {
threadPool.start();
scheduleRankUpdate();
return super.initialize();
}
@Override
public boolean terminate() {
threadPool.stop(); // To stop more threads from running
threadPool.awaitTermination(1000); // Waits for them to complete executing
return super.terminate();
}
int leftoverPoints(float progress, int points) {
return (int) ((progress - 100) / 100 * (float) points);
}
boolean isRankDown(float oldProgress, float newProgress) {
return oldProgress + newProgress < 0;
}
int nextUpdateTime(LocalDate now) {
LocalDate nextUpdateDate = now.with(updateWeekDay);
return (int) nextUpdateDate.toEpochSecond(updateTime, updateOffset);
}
boolean isDecayRank(int rank) {
return rank >= 7; // Lieutenant and above has decay
}
float rankProgress(float currentProgress, float decay, int currentRank, int points) {
float progress = currentProgress - decay;
progress += (float) points / (currentRank * 100.0f);
return progress;
}
boolean isFactionEligible(PvpFaction killerFaction, PvpFaction corpseFaction, boolean specialForces) {
return killerFaction != PvpFaction.NEUTRAL && corpseFaction != PvpFaction.NEUTRAL && killerFaction != corpseFaction && specialForces;
}
byte makeMultiplier(boolean specialForces, boolean player) {
byte multiplier = 1;
if (specialForces) {
multiplier += 1;
}
if (player) {
multiplier += 18;
}
return multiplier;
}
int baseForDifficulty(CreatureDifficulty difficulty) {
switch (difficulty) {
case NORMAL:
return 5;
case ELITE:
return 10;
case BOSS:
return 15;
default:
throw new IllegalArgumentException("Unhandled CreatureDifficulty: " + difficulty);
}
}
int pointsGranted(int base, byte multiplier) {
return base * multiplier;
}
private void loadRankAbilities() {
String what = "rank abilities";
long startTime = StandardLog.onStartLoad(what);
try (SdbLoader.SdbResultSet set = SdbLoader.load(new File("serverdata/gcw/abilities.sdb"))) {
while (set.next()) {
int rank = (int) set.getInt("rank");
String imperial = set.getText("imperial");
String rebel = set.getText("rebel");
String[] abilities = new String[2];
abilities[IMPERIAL_INDEX] = imperial;
abilities[REBEL_INDEX] = rebel;
rankAbilities.put(rank, abilities);
}
} catch (IOException e) {
Log.e(e);
}
StandardLog.onEndLoad(rankAbilities.size(), what, startTime);
}
private void scheduleRankUpdate() {
LocalDate now = LocalDate.now();
int nowEpoch = (int) now.toEpochSecond(updateTime, updateOffset);
rankEpoch = nextUpdateTime(now); // Is in the future
int delay = (rankEpoch - nowEpoch) * 1000; // Must be milliseconds
threadPool.execute(delay, this::updateRanks);
playerObjects.forEach(this::updateTimer);
}
private void updateTimer(PlayerObject playerObject) {
playerObject.setGcwNextUpdate(rankEpoch);
}
private void changeRank(PlayerObject playerObject, int newRank) {
assert (newRank < 1 || newRank > 12);
int oldRank = playerObject.getCurrentRank();
assert (oldRank > 1);
int highestRank = Math.max(oldRank, newRank);
int lowestRank = Math.min(oldRank, newRank);
CreatureObject creature = (CreatureObject) playerObject.getParent();
PvpFaction faction = creature.getPvpFaction();
int abilityIndex = faction == PvpFaction.IMPERIAL ? IMPERIAL_INDEX : REBEL_INDEX;
playerObject.setCurrentRank(newRank);
if (oldRank > newRank) {
// They've been demoted
for (int rank = highestRank; rank >= lowestRank; rank--) {
if (!isDecayRank(rank)) {
break;
}
String ability = rankAbilities.get(rank)[abilityIndex];
creature.removeAbility(ability);
}
} else if (oldRank < newRank) {
// They've been promoted
for (int rank = lowestRank; rank <= highestRank; rank++) {
if (!isDecayRank(rank)) {
continue;
}
String ability = rankAbilities.get(rank)[abilityIndex];
creature.addAbility(ability);
}
}
}
private void updateRank(PlayerObject playerObject) {
int currentRank = playerObject.getCurrentRank();
float oldProgress = playerObject.getRankProgress();
int points = playerObject.getGcwPoints();
float decay = 0;
if (isDecayRank(currentRank)) {
decay = currentRank;
}
float newProgress = rankProgress(oldProgress, decay, currentRank, points);
if (newProgress >= 100) {
int promotion = playerObject.getCurrentRank() + 1;
changeRank(playerObject, promotion);
// Leftover points carry over
int leftoverPoints = leftoverPoints(newProgress, points);
// Calculate progress within the new rank using leftover points
float nextRankProgress = rankProgress(newProgress, 0, promotion, leftoverPoints);
playerObject.setRankProgress(nextRankProgress);
} else if (newProgress > 0) {
// Set their new progress
playerObject.setRankProgress(newProgress);
} else if (newProgress < 0 && isRankDown(oldProgress, newProgress)) {
int demotion = playerObject.getCurrentRank() - 1;
changeRank(playerObject, demotion);
// Push their progress backwards in the new rank
int leftoverPoints = leftoverPoints(newProgress, points);
// Calculate progress within the new rank using leftover points
float nextRankProgress = 100 - rankProgress(newProgress, 0, demotion, leftoverPoints);
playerObject.setRankProgress(nextRankProgress);
}
int kills = playerObject.getPvpKills();
// Reset current kills and points
playerObject.setGcwPoints(0);
playerObject.setPvpKills(0);
// Add current stats to lifetime stats
playerObject.setLifetimeGcwPoints(playerObject.getLifetimeGcwPoints() + points);
playerObject.setLifetimePvpKills(playerObject.getLifetimePvpKills() + kills);
// TODO award GCW abilities
}
private void updateRanks() {
playerObjects.forEach(playerObject -> {
if (playerObject.getCurrentRank() > 0)
updateRank(playerObject);
});
scheduleRankUpdate(); // Schedule next rank update
}
private void grantPoints(ProsePackage prose, PlayerObject receiver, int points) {
receiver.setGcwPoints(receiver.getGcwPoints() + points);
SystemMessageIntent.broadcastPersonal(receiver.getOwner(), prose);
}
private void handleCivilWarPointIntent(CivilWarPointIntent cwpi) {
int points = cwpi.getPoints();
PlayerObject receiver = cwpi.getReceiver();
ProsePackage prose = new ProsePackage(new StringId("gcw", "gcw_rank_generic_point_grant"), "DI", points);
grantPoints(prose, receiver, points);
}
private void handleCreatureKilledIntent(CreatureKilledIntent cki) {
CreatureObject corpseCreature = cki.getCorpse();
CreatureObject killerCreature = cki.getKiller();
PvpFaction killerFaction = killerCreature.getPvpFaction();
if (!killerCreature.isPlayer()) {
return;
}
boolean specialForces = corpseCreature.getPvpStatus() == PvpStatus.SPECIALFORCES;
if (!isFactionEligible(killerFaction, corpseCreature.getPvpFaction(), specialForces)) {
return;
}
PlayerObject killerPlayer = killerCreature.getPlayerObject();
byte multiplier = makeMultiplier(specialForces, corpseCreature.isPlayer());
int base = baseForDifficulty(corpseCreature.getDifficulty());
int granted = pointsGranted(base, multiplier);
ProsePackage prose;
if (specialForces) {
// Increment kill counter
killerPlayer.setPvpKills(killerPlayer.getPvpKills() + 1);
// Determine which effect and sound to play
String effectFile;
String soundFile;
if (killerFaction == PvpFaction.REBEL) {
effectFile = "clienteffect/holoemote_rebel.cef";
soundFile = "sound/music_themequest_victory_rebel.snd";
} else {
effectFile = "clienteffect/holoemote_imperial.cef";
soundFile = "sound/music_themequest_victory_imperial.snd";
}
// PvP GCW point system message
prose = new ProsePackage("StringId", new StringId("gcw", "gcw_rank_pvp_kill_point_grant"), "DI", granted, "TT", corpseCreature
.getObjectName());
// Send visual effect to killer and everyone around
killerCreature.sendObserversAndSelf(new PlayClientEffectObjectMessage(effectFile, "head", killerCreature.getObjectId(), ""));
// Send sound to just to the killer
killerCreature.sendSelf(new PlayMusicMessage(0, soundFile, 0, false));
// TODO planetary control points?
} else {
// NPC GCW point system message
prose = new ProsePackage(new StringId("gcw", "gcw_rank_generic_point_grant"), "DI", granted);
}
// Increment GCW point counter
grantPoints(prose, killerPlayer, granted);
}
private void handleDestroyObjectIntent(DestroyObjectIntent doi) {
SWGObject object = doi.getObject();
if (!(object instanceof PlayerObject)) {
return;
}
playerObjects.remove(object);
}
private void handleObjectCreatedIntent(ObjectCreatedIntent oci) {
SWGObject object = oci.getObject();
if (!(object instanceof PlayerObject)) {
return;
}
PlayerObject playerObject = (PlayerObject) object;
// Let's make sure the timer's displayed for them
updateTimer(playerObject);
playerObjects.add(playerObject);
}
}
@@ -32,7 +32,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import com.projectswg.common.concurrency.PswgScheduledThreadPool;
import com.projectswg.common.control.Service;
import com.projectswg.common.control.Manager;
import com.projectswg.common.data.encodables.tangible.PvpFaction;
import com.projectswg.common.data.encodables.tangible.PvpFlag;
import com.projectswg.common.data.encodables.tangible.PvpStatus;
@@ -45,15 +45,17 @@ import com.projectswg.holocore.resources.objects.creature.CreatureObject;
import com.projectswg.holocore.resources.objects.tangible.TangibleObject;
import com.projectswg.holocore.resources.player.Player;
public final class FactionService extends Service {
public final class FactionManager extends Manager {
private final Map<TangibleObject, Future<?>> statusChangers;
private final PswgScheduledThreadPool executor;
public FactionService() {
public FactionManager() {
statusChangers = new ConcurrentHashMap<>();
executor = new PswgScheduledThreadPool(1, "faction-service");
addChildService(new CivilWarService());
registerForIntent(FactionIntent.class, this::handleFactionIntent);
}
@@ -97,7 +99,13 @@ public final class FactionService extends Service {
if(target.getBaselineType() == BaselineType.CREO && target.getPvpFaction() != PvpFaction.NEUTRAL) {
// We're given rank 1 upon joining a non-neutral faction
((CreatureObject) target).setFactionRank((byte) 1);
CreatureObject creatureTarget = (CreatureObject) target;
if (!creatureTarget.isPlayer()) {
return;
}
creatureTarget.getPlayerObject().setCurrentRank(1);
}
}
@@ -36,7 +36,7 @@ import com.projectswg.holocore.services.commands.CommandService;
import com.projectswg.holocore.services.commands.EntertainmentService;
import com.projectswg.holocore.services.crafting.CraftingManager;
import com.projectswg.holocore.services.experience.ExperienceManager;
import com.projectswg.holocore.services.faction.FactionService;
import com.projectswg.holocore.services.faction.FactionManager;
import com.projectswg.holocore.services.group.GroupService;
import com.projectswg.holocore.services.sui.SuiService;
@@ -49,7 +49,7 @@ public class GameManager extends Manager {
addChildService(new CollectionService());
addChildService(new CollectionBadgeManager());
addChildService(new EnvironmentService());
addChildService(new FactionService());
addChildService(new FactionManager());
addChildService(new GroupService());
addChildService(new SkillModService());
addChildService(new EntertainmentService());
@@ -27,6 +27,7 @@
***********************************************************************************/
package com.projectswg.holocore.services;
import com.projectswg.holocore.services.faction.TestCivilWarService;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@@ -38,6 +39,7 @@ import com.projectswg.holocore.services.player.TestPlayer;
@RunWith(Suite.class)
@SuiteClasses({
TestCivilWarService.class,
TestRareLootService.class,
TestCrafting.class,
TestPlayer.class,
@@ -0,0 +1,106 @@
/************************************************************************************
* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create an emulator which will provide a server for players to *
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
* This file is part of Holocore. *
* *
* -------------------------------------------------------------------------------- *
* *
* Holocore is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* Holocore is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.holocore.services.faction;
import com.projectswg.common.data.encodables.tangible.PvpFaction;
import com.projectswg.holocore.resources.objects.creature.CreatureDifficulty;
import org.junit.Assert;
import org.junit.Test;
import java.time.LocalDate;
public class TestCivilWarService {
private final CivilWarService service;
public TestCivilWarService() {
service = new CivilWarService();
}
@Test
public void testIsFactionEligible() {
Assert.assertTrue(service.isFactionEligible(PvpFaction.REBEL, PvpFaction.IMPERIAL, true));
Assert.assertFalse(service.isFactionEligible(PvpFaction.NEUTRAL, PvpFaction.NEUTRAL, false));
Assert.assertFalse(service.isFactionEligible(PvpFaction.NEUTRAL, PvpFaction.REBEL, false));
Assert.assertFalse(service.isFactionEligible(PvpFaction.NEUTRAL, PvpFaction.IMPERIAL, false));
Assert.assertFalse(service.isFactionEligible(PvpFaction.REBEL, PvpFaction.REBEL, true));
}
@Test
public void testMakeMultiplier() {
Assert.assertEquals(1, service.makeMultiplier(false, false));
Assert.assertEquals(2, service.makeMultiplier(true, false));
Assert.assertEquals(19, service.makeMultiplier(false, true));
Assert.assertEquals(20, service.makeMultiplier(true, true));
}
@Test
public void testBaseForDifficulty() {
Assert.assertEquals(5, service.baseForDifficulty(CreatureDifficulty.NORMAL));
Assert.assertEquals(10, service.baseForDifficulty(CreatureDifficulty.ELITE));
Assert.assertEquals(15, service.baseForDifficulty(CreatureDifficulty.BOSS));
}
@Test
public void testPointsGranted() {
Assert.assertEquals(200, service.pointsGranted(10, (byte) 20));
}
@Test
public void testNextUpdateTime() {
LocalDate now = LocalDate.of(2018, 2, 19); // It's a Monday
LocalDate rankDay = LocalDate.of(2018, 2, 23); // It's a Friday
int expectedRankTime = 1519344000; // The Friday that week, at 00:00 UTC (night between thursday and friday)
Assert.assertEquals(expectedRankTime, service.nextUpdateTime(now));
Assert.assertEquals(expectedRankTime, service.nextUpdateTime(rankDay));
}
@Test
public void testIsDecayRank() {
Assert.assertFalse(service.isDecayRank(6));
Assert.assertTrue(service.isDecayRank(7));
}
@Test
public void testRankProgress() {
Assert.assertEquals(2.86f, service.rankProgress(10.0f, 20.0f, 7, 9000), 1);
}
@Test
public void testIsRankDown() {
Assert.assertFalse(service.isRankDown(40, 50));
Assert.assertTrue(service.isRankDown(3, -10));
}
@Test
public void testLeftoverPoints() {
Assert.assertEquals(6000, service.leftoverPoints(130, 20000));
}
}