Merge pull request #1322 from madsboddum/entertainment_service_kt

EntertainmentService Java -> Kotlin conversion
This commit is contained in:
Josh
2023-06-04 11:23:04 -05:00
committed by GitHub
11 changed files with 486 additions and 483 deletions
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* Copyright (c) 2023 /// 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. *
@@ -26,43 +26,43 @@
***********************************************************************************/
package com.projectswg.holocore.intents.gameplay.entertainment.dance;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject;
import com.projectswg.holocore.resources.support.global.player.Player;
import me.joshlarson.jlcommon.control.Intent;
public class DanceIntent extends Intent {
private final String danceName;
private final CreatureObject creatureObject;
private final Player player;
private final boolean changeDance;
/**
* Start dancing
*
* @param danceName
* @param creatureObject
* @param player
*/
public DanceIntent(String danceName, CreatureObject creatureObject, boolean changeDance) {
public DanceIntent(String danceName, Player player, boolean changeDance) {
this.danceName = danceName;
this.creatureObject = creatureObject;
this.player = player;
this.changeDance = changeDance;
}
/**
* Stop dancing
*
* @param creatureObject
* @param player
*/
public DanceIntent(CreatureObject creatureObject) {
this(null, creatureObject, false);
public DanceIntent(Player player) {
this(null, player, false);
}
public String getDanceName() {
return danceName;
}
public CreatureObject getCreatureObject() {
return creatureObject;
public Player getPlayer() {
return player;
}
public boolean isStartDance() {
return danceName != null;
}
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* Copyright (c) 2023 /// 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. *
@@ -26,23 +26,23 @@
***********************************************************************************/
package com.projectswg.holocore.intents.gameplay.entertainment.dance;
import com.projectswg.holocore.resources.support.global.player.Player;
import com.projectswg.holocore.resources.support.objects.swg.SWGObject;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject;
import me.joshlarson.jlcommon.control.Intent;
public class WatchIntent extends Intent {
private final CreatureObject actor;
private final Player actor;
private final SWGObject target;
private final boolean startWatch;
public WatchIntent(CreatureObject actor, SWGObject target, boolean startWatch) {
public WatchIntent(Player actor, SWGObject target, boolean startWatch) {
this.actor = actor;
this.target = target;
this.startWatch = startWatch;
}
public CreatureObject getActor() {
public Player getActor() {
return actor;
}
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* Copyright (c) 2023 /// 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. *
@@ -50,7 +50,7 @@ public class KneelCmdCallback implements ICmdCallback {
}
if (creature.isPerforming()) {
new DanceIntent(player.getCreatureObject()).broadcast();
new DanceIntent(player).broadcast();
} else {
creature.setPosture(Posture.CROUCHED);
creature.setMovementPercent(0);
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* Copyright (c) 2023 /// 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. *
@@ -51,7 +51,7 @@ public class ProneCmdCallback implements ICmdCallback {
}
if (creature.isPerforming()) {
new DanceIntent(player.getCreatureObject()).broadcast();
new DanceIntent(player).broadcast();
} else {
creature.setPosture(Posture.PRONE);
creature.setTurnScale(1);
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* Copyright (c) 2023 /// 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. *
@@ -53,7 +53,7 @@ public class StandCmdCallback implements ICmdCallback {
if (creature.isPerforming()) {
// Ziggy: When you move while dancing, the client wants to execute /stand instead of /stopDance. Blame SOE.
new DanceIntent(player.getCreatureObject()).broadcast();
new DanceIntent(player).broadcast();
} else {
creature.clearStatesBitmask(CreatureState.SITTING_ON_CHAIR);
creature.setPosture(Posture.UPRIGHT);
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* Copyright (c) 2023 /// 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. *
@@ -70,12 +70,12 @@ public class StartDanceCallback implements ICmdCallback {
int selection = SuiListBox.getSelectedRow(parameters);
String selectedDanceName = listBox.getListItem(selection).getName().toLowerCase(Locale.ENGLISH);
new DanceIntent(selectedDanceName, player.getCreatureObject(), changeDance).broadcast();
new DanceIntent(selectedDanceName, player, changeDance).broadcast();
});
listBox.display(player);
} else {
new DanceIntent(args, player.getCreatureObject(), changeDance).broadcast();
new DanceIntent(args, player, changeDance).broadcast();
}
}
}
@@ -1,3 +1,29 @@
/***********************************************************************************
* Copyright (c) 2023 /// 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.resources.support.global.commands.callbacks.generic;
import com.projectswg.holocore.intents.gameplay.entertainment.dance.DanceIntent;
@@ -10,7 +36,7 @@ public final class CmdStopDance implements ICmdCallback {
@Override
public void execute(@NotNull Player player, SWGObject target, @NotNull String args) {
new DanceIntent(player.getCreatureObject()).broadcast();
new DanceIntent(player).broadcast();
}
}
@@ -1,3 +1,29 @@
/***********************************************************************************
* Copyright (c) 2023 /// 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.resources.support.global.commands.callbacks.generic;
import com.projectswg.holocore.intents.gameplay.entertainment.dance.WatchIntent;
@@ -19,7 +45,7 @@ public final class CmdStopWatching implements ICmdCallback {
return;
}
new WatchIntent(player.getCreatureObject(), target, false).broadcast();
new WatchIntent(player, target, false).broadcast();
}
}
@@ -1,3 +1,29 @@
/***********************************************************************************
* Copyright (c) 2023 /// 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.resources.support.global.commands.callbacks.generic;
import com.projectswg.holocore.intents.gameplay.entertainment.dance.WatchIntent;
@@ -19,7 +45,7 @@ public final class CmdWatch implements ICmdCallback {
return;
}
new WatchIntent(player.getCreatureObject(), target, true).broadcast();
new WatchIntent(player, target, true).broadcast();
}
}
@@ -1,453 +0,0 @@
/***********************************************************************************
* 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.services.gameplay.entertainment;
import com.projectswg.common.data.encodables.oob.ProsePackage;
import com.projectswg.common.data.encodables.oob.StringId;
import com.projectswg.common.data.encodables.tangible.Posture;
import com.projectswg.common.data.location.Location;
import com.projectswg.common.network.packets.swg.zone.object_controller.Animation;
import com.projectswg.holocore.intents.gameplay.entertainment.dance.DanceIntent;
import com.projectswg.holocore.intents.gameplay.entertainment.dance.FlourishIntent;
import com.projectswg.holocore.intents.gameplay.entertainment.dance.WatchIntent;
import com.projectswg.holocore.intents.gameplay.player.experience.ExperienceIntent;
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent;
import com.projectswg.holocore.intents.support.global.zone.PlayerEventIntent;
import com.projectswg.holocore.intents.support.global.zone.PlayerTransformedIntent;
import com.projectswg.holocore.resources.support.data.server_info.StandardLog;
import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader;
import com.projectswg.holocore.resources.support.data.server_info.loader.PerformanceLoader.PerformanceInfo;
import com.projectswg.holocore.resources.support.global.player.Player;
import com.projectswg.holocore.resources.support.objects.swg.SWGObject;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @author Mads
*/
public class EntertainmentService extends Service {
// TODO: when performing, make NPCs in a radius of x look towards the player (?) and clap. When they stop, turn back (?) and stop clapping
private static final byte XP_CYCLE_RATE = 10;
private static final byte WATCH_RADIUS = 20;
private final Map<Long, Performance> performerMap;
private final ScheduledExecutorService executorService;
public EntertainmentService() {
performerMap = new HashMap<>(); // TODO synchronize access?
executorService = Executors.newSingleThreadScheduledExecutor();
}
@Override
public boolean terminate() {
executorService.shutdownNow();
return super.terminate();
}
@IntentHandler
private void handleDanceIntent(DanceIntent di) {
CreatureObject dancer = di.getCreatureObject();
String danceName = di.getDanceName();
if (di.isStartDance()) {
// This intent wants the creature to start dancing
// If we're changing dance, allow them to do so
boolean changeDance = di.isChangeDance();
if (!changeDance && dancer.isPerforming()) {
new SystemMessageIntent(dancer.getOwner(), "@performance:already_performing_self").broadcast();
} else if (DataLoader.Companion.performances().getPerformanceByName(danceName) != null) {
// The dance name is valid.
if (dancer.hasCommand("startDance+" + danceName)) {
if (changeDance) { // If they're changing dance, we just need to change their animation.
changeDance(dancer, danceName);
} else { // Otherwise, they should begin performing now
startDancing(dancer, danceName);
}
} else {
// This creature doesn't have the ability to perform this dance.
new SystemMessageIntent(dancer.getOwner(), "@performance:dance_lack_skill_self").broadcast();
}
} else {
// This dance name is invalid
new SystemMessageIntent(dancer.getOwner(), "@performance:dance_unknown_self").broadcast();
}
} else {
// This intent wants the creature to stop dancing
stopDancing(dancer);
}
}
@IntentHandler
private void handlePlayerEventIntent(PlayerEventIntent pei) {
CreatureObject creature = pei.getPlayer().getCreatureObject();
if (creature == null)
return;
switch (pei.getEvent()) {
case PE_LOGGED_OUT:
// Don't keep giving them XP if they log out
if (isEntertainer(creature) && creature.getPosture().equals(Posture.SKILL_ANIMATING)) {
cancelExperienceTask(creature);
}
break;
case PE_ZONE_IN_SERVER:
// We need to check if they're dancing in order to start giving them XP
if (isEntertainer(creature) && creature.getPosture().equals(Posture.SKILL_ANIMATING)) {
scheduleExperienceTask(creature, DataLoader.Companion.performances().getPerformanceByDanceId(Integer.parseInt(creature.getAnimation().replace("dance_", ""))).getPerformanceName());
}
break;
case PE_DISAPPEAR:
// If a spectator disappears, they need to stop watching and be removed from the audience
long performerId = creature.getPerformanceListenTarget();
if (performerId != 0 && performerMap.get(performerId).removeSpectator(creature)) {
stopWatching(creature, false);
}
// If a performer disappears, the audience needs to be cleared
// They're also removed from the map of active performers.
if (isEntertainer(creature) && creature.isPerforming()) {
performerMap.get(creature.getObjectId()).clearSpectators();
performerMap.remove(creature.getObjectId());
}
break;
default:
break;
}
}
@IntentHandler
private void handleFlourishIntent(FlourishIntent fi) {
Player performer = fi.getPerformer();
CreatureObject performerObject = performer.getCreatureObject();
if (performerObject.getPerformanceCounter() != 0)
return;
performerObject.setPerformanceCounter(1);
// Send the flourish animation to the owner of the creature and owners of creatures observing
performerObject.sendObservers(new Animation(performerObject.getObjectId(), fi.getFlourishName()));
new SystemMessageIntent(performer, "@performance:flourish_perform").broadcast();
}
@IntentHandler
private void handleWatchIntent(WatchIntent wi) {
SWGObject target = wi.getTarget();
if (target instanceof CreatureObject) {
CreatureObject actor = wi.getActor();
CreatureObject creature = (CreatureObject) target;
Player actorOwner = actor.getOwner();
if (!isEntertainer(creature)) {
// We can't watch non-entertainers - do nothing
return;
}
if (creature.isPlayer()) {
if (creature.isPerforming()) {
Performance performance = performerMap.get(creature.getObjectId());
if (wi.isStartWatch()) {
if (performance.addSpectator(actor)) {
startWatching(actor, creature);
}
} else {
if (performance.removeSpectator(actor)) {
stopWatching(actor, true);
}
}
} else {
// While this is a valid target for watching, the target is currently not performing.
new SystemMessageIntent(actorOwner, new ProsePackage(new StringId("performance", "dance_watch_not_dancing"), "TT", creature.getObjectName())).broadcast();
}
} else {
// You can't watch NPCs, regardless of whether they're dancing or not
new SystemMessageIntent(actorOwner, "@performance:dance_watch_npc").broadcast();
}
}
}
@IntentHandler
private void handlePlayerTransformedIntent(PlayerTransformedIntent pti) {
CreatureObject movedPlayer = pti.getPlayer();
long performanceListenTarget = movedPlayer.getPerformanceListenTarget();
if (performanceListenTarget != 0) {
// They're watching a performer!
Performance performance = performerMap.get(performanceListenTarget);
if (performance == null) {
Log.e("Couldn't perform range check on %s, because there was no performer with object ID %d", movedPlayer, performanceListenTarget);
return;
}
CreatureObject performer = performance.getPerformer();
Location performerLocation = performer.getWorldLocation();
Location movedPlayerLocation = pti.getPlayer().getWorldLocation(); // Ziggy: The newLocation in PlayerTransformedIntent isn't the world location, which is what we need here
if (!movedPlayerLocation.isWithinDistance(performerLocation, WATCH_RADIUS)) {
// They moved out of the defined range! Make them stop watching
if (performance.removeSpectator(movedPlayer)) {
stopWatching(movedPlayer, true);
} else {
Log.w("%s ran out of range of %s, but couldn't stop watching because they weren't watching in the first place", movedPlayer, performer);
}
}
}
}
/**
* Checks if the {@code CreatureObject} is a Novice Entertainer.
*
* @param performer
* @return true if {@code performer} is a Novice Entertainer and false if not
*/
private boolean isEntertainer(CreatureObject performer) {
return performer.hasSkill("social_entertainer_novice"); // First entertainer skillbox
}
private void scheduleExperienceTask(CreatureObject performer, String performanceName) {
Log.d("Scheduled %s to receive XP every %d seconds", performer, XP_CYCLE_RATE);
synchronized (performerMap) {
long performerId = performer.getObjectId();
Future<?> future = executorService.scheduleAtFixedRate(new EntertainerExperience(performer), XP_CYCLE_RATE, XP_CYCLE_RATE, TimeUnit.SECONDS);
// If they went LD but came back before disappearing
if (performerMap.containsKey(performerId)) {
Performance performance = performerMap.get(performerId);
performance.setFuture(future);
} else {
performerMap.put(performer.getObjectId(), new Performance(performer, future, performanceName));
}
}
}
private void cancelExperienceTask(CreatureObject performer) {
Log.d("%s no longer receives XP every %d seconds", performer, XP_CYCLE_RATE);
synchronized (performerMap) {
Performance performance = performerMap.get(performer.getObjectId());
if (performance == null) {
Log.e("Couldn't cancel experience task for %s because they weren't found in performerMap", performer);
return;
}
Future<?> future = performance.getFuture();
// TODO null check?
// TODO use return result?
future.cancel(false); // Running tasks are allowed to finish.
}
}
private void startDancing(CreatureObject dancer, String danceName) {
dancer.setAnimation("dance_" + DataLoader.Companion.performances().getPerformanceByName(danceName).getDanceVisualId());
dancer.setPerformanceId(0); // 0 - anything else will make it look like we're playing music
dancer.setPerformanceCounter(0);
dancer.setPerforming(true);
dancer.setPosture(Posture.SKILL_ANIMATING);
scheduleExperienceTask(dancer, danceName);
new SystemMessageIntent(dancer.getOwner(), "@performance:dance_start_self").broadcast();
}
private void stopDancing(CreatureObject dancer) {
if (dancer.isPerforming()) {
dancer.setPerforming(false);
dancer.setPosture(Posture.UPRIGHT);
dancer.setPerformanceCounter(0);
dancer.setAnimation("");
// Non-entertainers don't receive XP and have no audience - ignore them
if (isEntertainer(dancer)) {
cancelExperienceTask(dancer);
performerMap.remove(dancer.getObjectId()).clearSpectators();
}
new SystemMessageIntent(dancer.getOwner(), "@performance:dance_stop_self").broadcast();
} else {
new SystemMessageIntent(dancer.getOwner(), "@performance:dance_not_performing").broadcast();
}
}
private void changeDance(CreatureObject dancer, String newPerformanceName) {
performerMap.get(dancer.getObjectId()).setPerformanceName(newPerformanceName);
dancer.setAnimation("dance_" + DataLoader.Companion.performances().getPerformanceByName(newPerformanceName).getPerformanceName());
}
private void startWatching(CreatureObject actor, CreatureObject creature) {
actor.setMoodAnimation("entertained");
new SystemMessageIntent(actor.getOwner(), new ProsePackage(new StringId("performance", "dance_watch_self"), "TT", creature.getObjectName())).broadcast();
actor.setPerformanceListenTarget(creature.getObjectId());
}
private void stopWatching(CreatureObject actor, boolean displaySystemMessage) {
actor.setMoodAnimation("neutral");
if (displaySystemMessage)
new SystemMessageIntent(actor.getOwner(), "@performance:dance_watch_stop_self").broadcast();
actor.setPerformanceListenTarget(0);
}
private class Performance {
private final CreatureObject performer;
private final Set<CreatureObject> audience;
private Future<?> future;
private String performanceName;
public Performance(CreatureObject performer, Future<?> future, String performanceName) {
this.performer = performer;
this.future = future;
this.performanceName = performanceName;
audience = new HashSet<>();
}
public CreatureObject getPerformer() {
return performer;
}
public Future<?> getFuture() {
return future;
}
public void setFuture(Future<?> future) {
this.future = future;
}
public String getPerformanceName() {
return performanceName;
}
public boolean addSpectator(CreatureObject spectator) {
return audience.add(spectator);
}
public boolean removeSpectator(CreatureObject spectator) {
return audience.remove(spectator);
}
public void clearSpectators() {
audience.forEach(spectator -> stopWatching(spectator, true));
audience.clear();
}
public void setPerformanceName(String performanceName) {
this.performanceName = performanceName;
}
}
/**
* Data pulled from the performance.iff table
*/
private static class PerformanceData {
private final String performanceId;
private final int flourishXpMod;
public PerformanceData(String performanceId, int flourishXpMod) {
this.performanceId = performanceId;
this.flourishXpMod = flourishXpMod;
}
public String getPerformanceId() {
return performanceId;
}
public int getFlourishXpMod() {
return flourishXpMod;
}
}
private class EntertainerExperience implements Runnable {
private final CreatureObject performer;
private EntertainerExperience(CreatureObject performer) {
this.performer = performer;
}
@Override
public void run() {
Performance performance = performerMap.get(performer.getObjectId());
if (performance == null) {
StandardLog.onPlayerError(this, performer, "is not in performerMap");
return;
}
String performanceName = performance.getPerformanceName();
PerformanceInfo performanceData = DataLoader.Companion.performances().getPerformanceByName(performanceName);
if (performanceData == null) {
StandardLog.onPlayerError(this, performer, "was performing unknown performance: '%s'", performanceName);
return;
}
int flourishXpMod = performanceData.getFlourishXpMod();
int performanceCounter = performer.getPerformanceCounter();
int xpGained = performanceCounter * flourishXpMod;
if (xpGained > 0) {
if (isEntertainer(performer)) {
if (isDancing()) {
new ExperienceIntent(performer, performer, "dance", xpGained, true).broadcast();
}
}
performer.setPerformanceCounter(performanceCounter - 1);
}
}
private boolean isDancing() {
return performer.getPerformanceId() == 0;
}
}
}
@@ -0,0 +1,378 @@
/***********************************************************************************
* Copyright (c) 2023 /// 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.gameplay.entertainment
import com.projectswg.common.data.encodables.oob.ProsePackage
import com.projectswg.common.data.encodables.oob.StringId
import com.projectswg.common.data.encodables.tangible.Posture
import com.projectswg.common.network.packets.swg.zone.object_controller.Animation
import com.projectswg.holocore.intents.gameplay.entertainment.dance.DanceIntent
import com.projectswg.holocore.intents.gameplay.entertainment.dance.FlourishIntent
import com.projectswg.holocore.intents.gameplay.entertainment.dance.WatchIntent
import com.projectswg.holocore.intents.gameplay.player.experience.ExperienceIntent
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
import com.projectswg.holocore.intents.support.global.zone.PlayerEventIntent
import com.projectswg.holocore.intents.support.global.zone.PlayerTransformedIntent
import com.projectswg.holocore.resources.support.data.server_info.StandardLog
import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader.Companion.performances
import com.projectswg.holocore.resources.support.global.player.Player
import com.projectswg.holocore.resources.support.global.player.PlayerEvent
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import me.joshlarson.jlcommon.log.Log
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
class EntertainmentService : Service() {
private val performerMap = mutableMapOf<Long, Performance>()
private val executorService = Executors.newSingleThreadScheduledExecutor()
override fun terminate(): Boolean {
executorService.shutdownNow()
return super.terminate()
}
@IntentHandler
private fun handleDanceIntent(di: DanceIntent) {
val player = di.player
val dancer = player.creatureObject
val danceName = di.danceName
if (di.isStartDance) {
// This intent wants the creature to start dancing
// If we're changing dance, allow them to do so
val changeDance = di.isChangeDance
if (!changeDance && dancer.isPerforming) {
SystemMessageIntent(player, "@performance:already_performing_self").broadcast()
} else if (performances().getPerformanceByName(danceName) != null) {
// The dance name is valid.
if (dancer.hasCommand("startDance+$danceName")) {
if (changeDance) { // If they're changing dance, we just need to change their animation.
changeDance(dancer, danceName)
} else { // Otherwise, they should begin performing now
startDancing(player, danceName)
}
} else {
// This creature doesn't have the ability to perform this dance.
SystemMessageIntent(player, "@performance:dance_lack_skill_self").broadcast()
}
} else {
// This dance name is invalid
SystemMessageIntent(player, "@performance:dance_unknown_self").broadcast()
}
} else {
// This intent wants the creature to stop dancing
stopDancing(player)
}
}
@IntentHandler
private fun handlePlayerEventIntent(pei: PlayerEventIntent) {
val player = pei.player
val creature = player.creatureObject ?: return
when (pei.event) {
PlayerEvent.PE_LOGGED_OUT -> handlePlayerLoggedOut(creature)
PlayerEvent.PE_ZONE_IN_SERVER -> handlePlayerZoneIn(creature)
PlayerEvent.PE_DISAPPEAR -> handlePlayerDisappear(player)
else -> {}
}
}
private fun handlePlayerDisappear(player: Player) {
val creature = player.creatureObject
// If a spectator disappears, they need to stop watching and be removed from the audience
val performerId = creature.performanceListenTarget
val performance = performerMap[performerId]
val spectating = performance?.removeSpectator(creature) == true
if (performerId != 0L && spectating) {
stopWatching(player, false)
}
// If a performer disappears, the audience needs to be cleared
// They're also removed from the map of active performers.
if (isEntertainer(creature) && creature.isPerforming) {
performerMap[creature.objectId]?.clearSpectators()
performerMap.remove(creature.objectId)
}
}
private fun handlePlayerZoneIn(creature: CreatureObject) {
if (isEntertainer(creature) && creature.posture == Posture.SKILL_ANIMATING) {
val danceId = creature.animation.replace("dance_", "").toInt()
val performanceByDanceId = performances().getPerformanceByDanceId(danceId)
if (performanceByDanceId != null) {
scheduleExperienceTask(
creature, performanceByDanceId.performanceName
)
}
}
}
private fun handlePlayerLoggedOut(creature: CreatureObject) {
if (isEntertainer(creature) && creature.posture == Posture.SKILL_ANIMATING) {
cancelExperienceTask(creature)
}
}
@IntentHandler
private fun handleFlourishIntent(fi: FlourishIntent) {
val performer = fi.performer
val performerObject = performer.creatureObject
if (performerObject.performanceCounter != 0) return
performerObject.performanceCounter = 1
// Send the flourish animation to the owner of the creature and owners of creatures observing
performerObject.sendObservers(Animation(performerObject.objectId, fi.flourishName))
SystemMessageIntent(performer, "@performance:flourish_perform").broadcast()
}
@IntentHandler
private fun handleWatchIntent(wi: WatchIntent) {
val target = wi.target
if (target is CreatureObject) {
val actor = wi.actor
if (!isEntertainer(target)) {
// We can't watch non-entertainers - do nothing
return
}
if (target.isPlayer) {
if (target.isPerforming) {
val performance = performerMap[target.objectId] ?: return
if (wi.isStartWatch) {
if (performance.addSpectator(actor.creatureObject)) {
startWatching(actor, target)
}
} else {
if (performance.removeSpectator(actor.creatureObject)) {
stopWatching(actor, true)
}
}
} else {
// While this is a valid target for watching, the target is currently not performing.
SystemMessageIntent(
actor, ProsePackage(StringId("performance", "dance_watch_not_dancing"), "TT", target.objectName)
).broadcast()
}
} else {
// You can't watch NPCs, regardless of whether they're dancing or not
SystemMessageIntent(actor, "@performance:dance_watch_npc").broadcast()
}
}
}
@IntentHandler
private fun handlePlayerTransformedIntent(pti: PlayerTransformedIntent) {
val movedPlayer = pti.player
val performanceListenTarget = movedPlayer.performanceListenTarget
if (performanceListenTarget != 0L) {
// They're watching a performer!
val performance = performerMap[performanceListenTarget]
if (performance == null) {
Log.e("Couldn't perform range check on %s, because there was no performer with object ID %d", movedPlayer, performanceListenTarget)
return
}
val performer = performance.performer
val performerLocation = performer.worldLocation
val movedPlayerLocation = pti.player.worldLocation // Ziggy: The newLocation in PlayerTransformedIntent isn't the world location, which is what we need here
if (!movedPlayerLocation.isWithinDistance(performerLocation, WATCH_RADIUS.toDouble())) {
// They moved out of the defined range! Make them stop watching
if (performance.removeSpectator(movedPlayer)) {
val player = movedPlayer.owner
if (player != null) {
stopWatching(player, true)
}
} else {
Log.w(
"%s ran out of range of %s, but couldn't stop watching because they weren't watching in the first place",
movedPlayer,
performer
)
}
}
}
}
/**
* Checks if the `CreatureObject` is a Novice Entertainer.
*
* @param performer
* @return true if `performer` is a Novice Entertainer and false if not
*/
private fun isEntertainer(performer: CreatureObject): Boolean {
return performer.hasSkill("social_entertainer_novice") // First entertainer skillbox
}
private fun scheduleExperienceTask(performer: CreatureObject, performanceName: String) {
Log.d("Scheduled %s to receive XP every %d seconds", performer, XP_CYCLE_RATE)
synchronized(performerMap) {
val performerId = performer.objectId
val future = executorService.scheduleAtFixedRate(
EntertainerExperience(performer),
XP_CYCLE_RATE.toLong(),
XP_CYCLE_RATE.toLong(),
TimeUnit.SECONDS
)
// If they went LD but came back before disappearing
val performance = performerMap[performerId]
if (performance != null) {
performance.future = future
} else {
performerMap.put(performer.objectId, Performance(performer, future, performanceName))
}
}
}
private fun cancelExperienceTask(performer: CreatureObject) {
Log.d("%s no longer receives XP every %d seconds", performer, XP_CYCLE_RATE)
synchronized(performerMap) {
val performance = performerMap[performer.objectId]
if (performance == null) {
Log.e("Couldn't cancel experience task for %s because they weren't found in performerMap", performer)
return
}
performance.future.cancel(false)
}
}
private fun startDancing(player: Player, danceName: String) {
val dancer = player.creatureObject
val performanceByName = performances().getPerformanceByName(danceName)
if (performanceByName == null) {
StandardLog.onPlayerEvent(this, dancer, "tried to start unknown dance %s", danceName)
return
}
val danceVisualId = performanceByName.danceVisualId
dancer.animation = "dance_$danceVisualId"
dancer.performanceId = 0 // 0 - anything else will make it look like we're playing music
dancer.performanceCounter = 0
dancer.isPerforming = true
dancer.posture = Posture.SKILL_ANIMATING
scheduleExperienceTask(dancer, danceName)
SystemMessageIntent(player, "@performance:dance_start_self").broadcast()
}
private fun stopDancing(player: Player) {
val dancer = player.creatureObject
if (dancer.isPerforming) {
dancer.isPerforming = false
dancer.posture = Posture.UPRIGHT
dancer.performanceCounter = 0
dancer.animation = ""
// Non-entertainers don't receive XP and have no audience - ignore them
if (isEntertainer(dancer)) {
cancelExperienceTask(dancer)
val performance = performerMap.remove(dancer.objectId)
performance?.clearSpectators()
}
SystemMessageIntent(player, "@performance:dance_stop_self").broadcast()
} else {
SystemMessageIntent(player, "@performance:dance_not_performing").broadcast()
}
}
private fun changeDance(dancer: CreatureObject, newPerformanceName: String) {
val performance = performerMap[dancer.objectId]
if (performance != null) {
performance.performanceName = newPerformanceName
val performanceByName = performances().getPerformanceByName(newPerformanceName)
if (performanceByName != null) {
dancer.animation = "dance_" + performanceByName.performanceName
}
}
}
private fun startWatching(player: Player, creature: CreatureObject) {
val actor = player.creatureObject
actor.moodAnimation = "entertained"
SystemMessageIntent(player, ProsePackage(StringId("performance", "dance_watch_self"), "TT", creature.objectName)).broadcast()
actor.performanceListenTarget = creature.objectId
}
private fun stopWatching(player: Player, displaySystemMessage: Boolean) {
val actor = player.creatureObject
actor.moodAnimation = "neutral"
if (displaySystemMessage) SystemMessageIntent(player, "@performance:dance_watch_stop_self").broadcast()
actor.performanceListenTarget = 0
}
private inner class Performance(val performer: CreatureObject, var future: Future<*>, var performanceName: String) {
private val audience = mutableSetOf<CreatureObject>()
fun addSpectator(spectator: CreatureObject): Boolean {
return audience.add(spectator)
}
fun removeSpectator(spectator: CreatureObject): Boolean {
return audience.remove(spectator)
}
fun clearSpectators() {
audience.mapNotNull { it.owner }.forEach(Consumer { player -> stopWatching(player, true) })
audience.clear()
}
}
private inner class EntertainerExperience(private val performer: CreatureObject) : Runnable {
override fun run() {
val performance = performerMap[performer.objectId]
if (performance == null) {
StandardLog.onPlayerError(this, performer, "is not in performerMap")
return
}
val performanceName = performance.performanceName
val performanceData = performances().getPerformanceByName(performanceName)
if (performanceData == null) {
StandardLog.onPlayerError(this, performer, "was performing unknown performance: '%s'", performanceName)
return
}
val flourishXpMod = performanceData.flourishXpMod
val performanceCounter = performer.performanceCounter
val xpGained = performanceCounter * flourishXpMod
if (xpGained > 0) {
if (isEntertainer(performer)) {
if (isDancing) {
ExperienceIntent(performer, performer, "dance", xpGained, true).broadcast()
}
}
performer.performanceCounter = performanceCounter - 1
}
}
private val isDancing: Boolean
get() = performer.performanceId == 0
}
companion object {
// TODO: when performing, make NPCs in a radius of x look towards the player (?) and clap. When they stop, turn back (?) and stop clapping
private const val XP_CYCLE_RATE: Byte = 10
private const val WATCH_RADIUS: Byte = 20
}
}