Remove the QuestManager code from PC

This commit is contained in:
Sam 2018-07-18 12:33:28 +01:00 committed by Alexander Meech
parent 4fb6608c3f
commit e45deb127c
16 changed files with 0 additions and 1188 deletions

View File

@ -1,69 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mineplex</groupId>
<artifactId>mineplex-parent</artifactId>
<version>dev-SNAPSHOT</version>
</parent>
<artifactId>mineplex-questmanager</artifactId>
<name>Mineplex.questmanager</name>
<description>A centralized service that selects daily quests</description>
<properties>
<version.guava>23.0</version.guava>
<version.jline>2.12</version.jline>
<version.spigot>1.8.8-1.9-SNAPSHOT</version.spigot>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mineplex-serverdata</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${version.guava}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jline</groupId>
<artifactId>jline</artifactId>
<version>${version.jline}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.mineplex</groupId>
<artifactId>spigot</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<minimizeJar>false</minimizeJar>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>mineplex.quest.daemon.QuestDaemon</mainClass>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,111 +0,0 @@
package mineplex.quest.client;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.bukkit.plugin.java.JavaPlugin;
import com.google.common.collect.ImmutableSet;
import mineplex.quest.client.event.QuestsUpdatedEvent;
import mineplex.quest.common.Quest;
import mineplex.quest.common.QuestSupplier;
import mineplex.quest.common.redis.PubSubChannels;
import mineplex.quest.common.redis.QuestTypeSerializer;
import mineplex.serverdata.redis.messaging.PubSubMessager;
/**
* Provides methods to retrieve currently active quests. Retrieves active quests from the
* centralized quest service via redis.
* <p>
* Intended to be thread-safe.
*/
public class RedisQuestSupplier implements QuestSupplier
{
private final JavaPlugin _plugin;
private final String _serverUniqueId;
private final PubSubMessager _pubSub;
private final ReadWriteLock _lock = new ReentrantReadWriteLock();
private final Set<Quest> _quests = new HashSet<>();
public RedisQuestSupplier(JavaPlugin plugin, PubSubMessager pubSub)
{
_plugin = plugin;
_pubSub = pubSub;
_serverUniqueId = plugin.getConfig().getString("serverstatus.name");
// update quests sent specifically to this server when it requests them (like on startup)
_pubSub.subscribe(PubSubChannels.QUEST_REQUEST_BASE + _serverUniqueId, this::updateQuests);
requestActiveQuests();
// update quests when received
_pubSub.subscribe(PubSubChannels.QUEST_SUPPLIER_CHANNEL, this::updateQuests);
}
private void updateQuests(String channel, String message)
{
_lock.writeLock().lock();
try
{
_quests.clear();
_quests.addAll(deserialize(message));
System.out.println("[QUEST-SUPPLIER] Quest update received from daemon, active quests: ");
_quests.forEach(q -> System.out.println("[QUEST-SUPPLIER] " + q.toString()));
// notify
_plugin.getServer().getPluginManager().callEvent(new QuestsUpdatedEvent(get()));
}
finally
{
_lock.writeLock().unlock();
}
}
private Set<Quest> deserialize(String json)
{
return QuestTypeSerializer.QUEST_GSON.fromJson(json, QuestTypeSerializer.QUEST_TYPE);
}
private void requestActiveQuests()
{
System.out.println("[QUEST-SUPPLIER] Requesting active quests from QuestDaemon");
// request current active quests, send server unique id so we can send a response just to this server
_pubSub.publish(PubSubChannels.QUEST_REQUEST_BASE, _serverUniqueId);
}
@Override
public Set<Quest> get()
{
_lock.readLock().lock();
try
{
return ImmutableSet.copyOf(_quests);
}
finally
{
_lock.readLock().unlock();
}
}
@Override
public Optional<Quest> getById(int uniquePersistentId)
{
_lock.readLock().lock();
try
{
return _quests.stream().filter(q -> q.getUniqueId() == uniquePersistentId).findFirst();
}
finally
{
_lock.readLock().unlock();
}
}
}

View File

@ -1,44 +0,0 @@
package mineplex.quest.client.event;
import java.util.Set;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import mineplex.quest.common.Quest;
/**
* An event called when the currently active quests are rotated at the end of a day.
*/
public class QuestsUpdatedEvent extends Event
{
private static final HandlerList HANDLERS = new HandlerList();
private final Set<Quest> _to;
public QuestsUpdatedEvent(Set<Quest> to)
{
_to = to;
}
/**
* @return The currently active quests.
*/
public Set<Quest> getActiveQuests()
{
return _to;
}
@Override
public HandlerList getHandlers()
{
return HANDLERS;
}
public static HandlerList getHandlerList()
{
return HANDLERS;
}
}

View File

@ -1,59 +0,0 @@
package mineplex.quest.common;
/**
* Implementation of baseline {@link Quest}.
*/
public class BaseQuest implements Quest
{
private final int _uniqueId;
private final String _name;
private final QuestRarity _rarity;
private final int _cost;
public BaseQuest(int uniqueId, String name, int cost, QuestRarity rarity)
{
_uniqueId = uniqueId;
_name = name;
_cost = cost;
_rarity = rarity;
}
@Override
public String getName()
{
return _name;
}
@Override
public int getUniqueId()
{
return _uniqueId;
}
@Override
public QuestRarity getRarity()
{
return _rarity;
}
@Override
public String getDataId()
{
return _uniqueId + "";
}
@Override
public boolean shouldRotate()
{
return _cost != -1;
}
@Override
public String toString()
{
return "BaseQuest [uniqueId=" + _uniqueId + ", name=" + _name + ", rarity=" + _rarity
+ ", cost=" + _cost + "]";
}
}

View File

@ -1,53 +0,0 @@
package mineplex.quest.common;
import mineplex.quest.daemon.QuestManager;
import mineplex.serverdata.data.Data;
import mineplex.serverdata.data.DataRepository;
/**
* A quest that can be completed by users for a reward.
*/
public interface Quest extends Data
{
/**
* Gets the name of this quest.
*
* @return The name of this quest.
*/
String getName();
/**
* Gets the unique persistent id for this quest. This id will be used to store quests per user
* and <b>should not be changed</b>.
*
* @return The unique persistent id for this quest.
*/
int getUniqueId();
/**
* Gets the {@link QuestRarity} of this quest.
*
* @return The rarity of this quest.
*/
QuestRarity getRarity();
/**
* Get the unique persistent id for this quest as a String. Intended to be used for storage
* within Redis via {@link DataRepository}.
* <p>
* Don't use this to get the quest unique id, use {@link Quest#getUniqueId()} instead.
*
* @return A string version of the unique persistent id for this quest.
*/
@Override
String getDataId();
/**
* Checks whether this quest should be selected by the {@link QuestManager} daemon process.
* Quests with a cost of -1 should not ever be selected as active quests by the daemon.
*
* @return <code>true</code> if this quest should be selected as an active quest, or
* <code>false</code> otherwise.
*/
boolean shouldRotate();
}

View File

@ -1,24 +0,0 @@
package mineplex.quest.common;
/**
* How rare a quest is. In other words, how often this quest should be chosen.
*/
public enum QuestRarity
{
COMMON(1.0),
RARE(0.5),
LEGENDARY(0.1)
;
private final double _weight;
private QuestRarity(double weight)
{
_weight = weight;
}
public double getWeight()
{
return _weight;
}
}

View File

@ -1,29 +0,0 @@
package mineplex.quest.common;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
/**
* Provides access to {@link Quest}s tracked by this QuestManager.
*/
public interface QuestSupplier extends Supplier<Set<Quest>>
{
/**
* Get an immutable set containing all of the currently active quests.
*/
@Override
Set<Quest> get();
/**
* Attempts to get the {@link Quest} matching the supplied persistent id.
*
* @param uniquePersistentId The unique id of the quest.
*
* @return An {@link Optional} describing the {@link Quest}, or an empty Optional if none is
* found.
*/
Optional<Quest> getById(int uniquePersistentId);
}

View File

@ -1,73 +0,0 @@
package mineplex.quest.common;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
import mineplex.quest.common.util.UtilGoogleSheet;
import mineplex.quest.daemon.QuestDaemon;
/**
* Provides access to all quests.
* <p>
* Loads quests from a google sheets json file.
*/
public class Quests
{
private static final String ALL_QUESTS_FILE = "QUESTS_SHEET";
private static final String QUEST_SHEET_KEY = "Quests";
private static final int UNIQUE_ID_COLUMN = 0;
private static final int NAME_COLUMN = 1;
private static final int COST_COLUMN = 4;
private static final int RARITY_COLUMN = 9;
public static final Set<Quest> QUESTS;
static
{
ImmutableSet.Builder<Quest> builder = ImmutableSet.<Quest>builder();
Map<String, List<List<String>>> sheets = UtilGoogleSheet.getSheetData(ALL_QUESTS_FILE);
List<List<String>> rows = sheets.getOrDefault(QUEST_SHEET_KEY, Collections.emptyList());
// get each row of spreadsheet, start at 1 since row 0 contains headers
for (int i = 1; i < rows.size(); i++)
{
List<String> row = rows.get(i);
// attempt to parse quest data we need
try
{
int uniqueId = Integer.parseInt(row.get(UNIQUE_ID_COLUMN));
String name = row.get(NAME_COLUMN);
int cost = Integer.parseInt(row.get(COST_COLUMN));
QuestRarity rarity = QuestRarity.valueOf(row.get(RARITY_COLUMN).toUpperCase());
Quest quest = new BaseQuest(uniqueId, name, cost, rarity);
builder.add(quest);
}
catch (Exception e)
{
QuestDaemon.log("Exception encountered while parsing quest sheet row: " + row + ", "
+ e.getMessage());
e.printStackTrace();
}
}
QUESTS = builder.build();
}
public static Optional<Quest> fromId(int uniqueId)
{
return QUESTS.stream().filter(quest -> quest.getUniqueId() == uniqueId).findFirst();
}
}

View File

@ -1,13 +0,0 @@
package mineplex.quest.common.redis;
/**
* Provides constants for Quests redis pub sub channels.
*/
public class PubSubChannels
{
public static final String QUEST_SUPPLIER_CHANNEL = "quest-manager";
public static final String QUEST_REQUEST_BASE = "quest-manager-request:";
}

View File

@ -1,39 +0,0 @@
package mineplex.quest.common.redis;
import mineplex.quest.common.Quest;
import mineplex.quest.common.Quests;
import mineplex.serverdata.Region;
import mineplex.serverdata.redis.RedisDataRepository;
import mineplex.serverdata.servers.ConnectionData;
/**
* A {@link RedisDataRepository} that can serialize & deserialize (and thus store & retrieve from
* redis) Quest instances.
*/
public class QuestRedisDataRepository extends RedisDataRepository<Quest>
{
public QuestRedisDataRepository(ConnectionData writeConn, ConnectionData readConn, Region region,
String elementLabel)
{
super(writeConn, readConn, region, Quest.class, elementLabel);
}
@Override
protected Quest deserialize(String json)
{
if (json == null || json.isEmpty())
{
return null;
}
return Quests.fromId(Integer.parseInt(json)).orElse(null);
}
@Override
protected String serialize(Quest quest)
{
return quest.getDataId();
}
}

View File

@ -1,36 +0,0 @@
package mineplex.quest.common.redis;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import mineplex.quest.common.Quest;
import mineplex.quest.common.Quests;
/**
* An implementation of a {@link JsonDeserializer} intended for use in {@link Gson}. Deserializes a
* {@link JsonElement} String into a Set<Quest>.
*/
public class QuestTypeDeserialiazer implements JsonDeserializer<Set<Quest>>
{
@Override
public Set<Quest> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException
{
String[] split = json.getAsString().split(QuestTypeSerializer.SEPARATOR);
return Arrays.stream(split).map(questId -> Quests.fromId(Integer.valueOf(questId)))
.filter(Optional::isPresent).map(Optional::get)
.collect(Collectors.toCollection(HashSet::new));
}
}

View File

@ -1,44 +0,0 @@
package mineplex.quest.common.redis;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.common.base.Joiner;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import mineplex.quest.common.Quest;
/**
* An implementation of a {@link JsonSerializer} intended for use in {@link Gson}. Serializes a
* Set<Quest> into a {@link JsonElement} String.
*/
public class QuestTypeSerializer implements JsonSerializer<Set<Quest>>
{
@SuppressWarnings("serial")
public static final Type QUEST_TYPE = new TypeToken<Set<Quest>>(){}.getType();
public static final Gson QUEST_GSON = new GsonBuilder()
.registerTypeAdapter(QuestTypeSerializer.QUEST_TYPE, new QuestTypeDeserialiazer())
.registerTypeAdapter(QuestTypeSerializer.QUEST_TYPE, new QuestTypeSerializer())
.create();
public static final String SEPARATOR = ",";
@Override
public JsonElement serialize(Set<Quest> src, Type typeOfSrc, JsonSerializationContext context)
{
StringBuilder builder = new StringBuilder();
Joiner.on(SEPARATOR).appendTo(builder,
src.stream().map(Quest::getDataId).collect(Collectors.toSet()));
return new JsonPrimitive(builder.toString());
}
}

View File

@ -1,62 +0,0 @@
package mineplex.quest.common.util;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.ThreadLocalRandom;
/**
* Provides random, weighted access to a collection of elements.
* <p>
* Intended to be thread-safe.
*
* @param <E> The generic type parameter of the elements.
*/
public class RandomCollection<E>
{
private final NavigableMap<Double, E> _map = Collections.synchronizedNavigableMap(new TreeMap<Double, E>());
private final Random _random;
private double total = 0;
public RandomCollection(Random random)
{
_random = random;
}
public RandomCollection()
{
this(ThreadLocalRandom.current());
}
public void addAll(Map<E, Double> values)
{
values.forEach(this::add);
}
public void add(E result, double weight)
{
if (weight <= 0)
{
return;
}
total += weight;
_map.put(total, result);
}
public E next()
{
double value = _random.nextDouble() * total;
return _map.ceilingEntry(value).getValue();
}
public Collection<E> values()
{
return _map.values();
}
}

View File

@ -1,75 +0,0 @@
package mineplex.quest.common.util;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* Provides utility methods for deserializing google sheets json files.
*/
public class UtilGoogleSheet
{
private static final File DATA_STORE_DIR = new File(
".." + File.separatorChar + ".." + File.separatorChar + "update" + File.separatorChar
+ "files");
public static Map<String, List<List<String>>> getSheetData(String name)
{
return getSheetData(new File(DATA_STORE_DIR + File.separator + name + ".json"));
}
public static Map<String, List<List<String>>> getSheetData(File file)
{
if (!file.exists())
{
return null;
}
Map<String, List<List<String>>> valuesMap = new HashMap<>();
try
{
JsonParser parser = new JsonParser();
JsonElement data = parser.parse(new FileReader(file));
JsonArray parent = data.getAsJsonObject().getAsJsonArray("data");
for (int i = 0; i < parent.size(); i++)
{
JsonObject sheet = parent.get(i).getAsJsonObject();
String name = sheet.get("name").getAsString();
JsonArray values = sheet.getAsJsonArray("values");
List<List<String>> valuesList = new ArrayList<>(values.size());
for (int j = 0; j < values.size(); j++)
{
List<String> list = new ArrayList<>();
Iterator<JsonElement> iterator = values.get(j).getAsJsonArray().iterator();
while (iterator.hasNext())
{
String value = iterator.next().getAsString();
list.add(value);
}
valuesList.add(list);
}
valuesMap.put(name, valuesList);
}
}
catch (FileNotFoundException e)
{}
return valuesMap;
}
}

View File

@ -1,180 +0,0 @@
package mineplex.quest.daemon;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.fusesource.jansi.AnsiConsole;
import com.google.common.base.Throwables;
import mineplex.serverdata.redis.messaging.PubSubJedisClient;
import mineplex.serverdata.redis.messaging.PubSubRouter;
import mineplex.serverdata.servers.ServerManager;
import jline.console.ConsoleReader;
/**
* Entry point for a {@link QuestManager} service.
*/
public class QuestDaemon
{
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM:dd:yyyy HH:mm:ss");
private static final Logger _logger = Logger.getLogger("QuestManager");
static
{
FileHandler fileHandler;
try
{
fileHandler = new FileHandler("monitor.log", true);
fileHandler.setFormatter(new Formatter()
{
@Override
public String format(LogRecord record)
{
return record.getMessage() + "\n";
}
});
_logger.addHandler(fileHandler);
_logger.setUseParentHandlers(false);
}
catch (SecurityException | IOException e)
{
log("COuld not initialize log file!");
log(e);
}
}
private volatile boolean _alive = true;
private QuestManager _questManager;
public static void main(String[] args)
{
try
{
new QuestDaemon().run();
System.exit(0);
}
catch (Throwable t)
{
log("Error in startup/console thread.");
log(t);
System.exit(1);
}
}
private void run() throws Exception
{
log("Starting QuestDaemon...");
_questManager = new QuestManager(new PubSubRouter(new PubSubJedisClient(
ServerManager.getMasterConnection(), ServerManager.getSlaveConnection())));
_questManager.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> _questManager.onShutdown()));
AnsiConsole.systemInstall();
ConsoleReader consoleReader = new ConsoleReader();
consoleReader.setExpandEvents(false);
String command;
while (_alive && (command = consoleReader.readLine(">")) != null)
{
try
{
if (command.equals("help"))
{
log("QuestManager commands:");
log("stop: Shuts down this QuestManager instance.");
log("clearactivequests: Clears active quests. New ones will be selected on this"
+ " instance's next iteration.");
log("clearrecentrequests: Clear recently selected quests. This effectively allows "
+ "any quest to be set to active, even ones selected within the past few days.");
log("getactivequests: Displays the currently active quests.");
}
else if (command.contains("stop"))
{
stopCommand();
}
else if (command.contains("clearactivequests"))
{
clearQuestsCommand();
}
else if (command.contains("clearrecentquests"))
{
clearRecentQuestsCommand();
}
else if (command.contains("getactivequests"))
{
getActiveQuestsCommand();
}
}
catch (Throwable t)
{
log("Exception encountered while executing command " + command + ": "
+ t.getMessage());
log(t);
}
}
}
private void stopCommand() throws Exception
{
log("Shutting down QuestDaemon...");
_alive = false;
System.exit(0);
}
private void clearQuestsCommand()
{
_questManager.clearActiveQuests();
log("Cleared active quests. New ones will be selected on this instance's next iteration.");
}
private void clearRecentQuestsCommand()
{
_questManager.clearRecentlyActiveQuests();
log("Cleared recently active quests. This means that any quest can be chosen to be active now, even ones selected within the past few days.");
}
private void getActiveQuestsCommand()
{
_questManager.displayActiveQuests();
}
public static void log(String message)
{
log(message, false);
}
public static void log(Throwable t)
{
log(Throwables.getStackTraceAsString(t));
}
public static void log(String message, boolean fileOnly)
{
_logger.info("[" + DATE_FORMAT.format(new Date()) + "] " + message);
if (!fileOnly)
{
System.out.println("[" + DATE_FORMAT.format(new Date()) + "] " + message);
}
}
public static String getLogPrefix(Object loggingClass)
{
return "[" + DATE_FORMAT.format(new Date()) + "] ";
}
}

View File

@ -1,277 +0,0 @@
package mineplex.quest.daemon;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import mineplex.quest.common.Quest;
import mineplex.quest.common.QuestRarity;
import mineplex.quest.common.Quests;
import mineplex.quest.common.redis.PubSubChannels;
import mineplex.quest.common.redis.QuestRedisDataRepository;
import mineplex.quest.common.redis.QuestTypeSerializer;
import mineplex.quest.common.util.RandomCollection;
import mineplex.serverdata.Region;
import mineplex.serverdata.data.DataRepository;
import mineplex.serverdata.redis.messaging.PubSubMessager;
import mineplex.serverdata.servers.ServerManager;
/**
* A centralized service that handles setting {@link Quest} instances as active for servers to
* display to players. Uses redis to notify servers of active quests changes and to store recently
* selected quests.
* <p>
* Uses {@link QuestRarity} to randomly select active quests based on relative weight.
*/
public class QuestManager extends Thread
{
private static final int RECENT_QUESTS_EXPIRE_SECONDS = (int) TimeUnit.DAYS.toSeconds(5);
private static final long SLEEP_MILLIS = TimeUnit.MINUTES.toMillis(1);
private static final int NUM_ACTIVE_QUESTS = 5;
private static final ZoneId EST_TIMEZONE = ZoneId.of("America/New_York");
private static final String LAST_UPDATE_FILE = "last-quest-update.dat";
// all quests, mapped from rarity weight to quest
private final RandomCollection<Quest> _quests = new RandomCollection<>();
// currently active quests
private final Set<Quest> _activeQuests = Collections.synchronizedSet(new HashSet<>());
// redis pubsub messager, used to publish active quests to servers
private final PubSubMessager _pubSub;
// redis repository to track recently selected quests, to prevent selecting a quest too soon
// after it's been active
private final DataRepository<Quest> _recentlySelectedQuestsRepo;
// the current date, e.g. the last date active quests were updated
private volatile LocalDate _currentDate;
// whether this instance is running or not
private volatile boolean _alive = true;
public QuestManager(PubSubMessager pubSub)
{
_pubSub = pubSub;
_recentlySelectedQuestsRepo = new QuestRedisDataRepository(
ServerManager.getMasterConnection(), ServerManager.getSlaveConnection(),
Region.currentRegion(), "recently-selected-quests");
_quests.addAll(Quests.QUESTS.stream().collect(
Collectors.toMap(q -> q, q -> q.getRarity().getWeight())));
loadLastActiveQuests();
if (_activeQuests.size() > 0)
{
QuestDaemon.log("Active quests loaded from file:");
_activeQuests.forEach(quest -> QuestDaemon.log(quest.toString()));
}
// listen for servers requesting active quests on startup
_pubSub.subscribe(PubSubChannels.QUEST_REQUEST_BASE, this::handleQuestRequest);
}
private void handleQuestRequest(String channel, String message)
{
QuestDaemon.log("Quests requestesd by server: " + message);
// first make sure we have some active quests selected
if (_activeQuests.isEmpty())
{
selectRandomQuests();
}
// send active quests to the server
String server = message;
publishActiveQuests(PubSubChannels.QUEST_REQUEST_BASE + server);
}
/**
* Loads last set active quests & the date they were set to active from a flat file, if the file
* exists.
*/
private void loadLastActiveQuests()
{
File file = new File(LAST_UPDATE_FILE);
if (!file.exists())
{
_currentDate = LocalDate.now(EST_TIMEZONE);
return;
}
try
{
List<String> lines = Files.readAllLines(Paths.get(file.getAbsolutePath()));
_currentDate = LocalDate.parse(lines.get(0));
if (lines.size() > 1)
{
for (int i = 1; i < lines.size(); i++)
{
int uniqueId = Integer.parseInt(lines.get(i));
Optional<Quest> quest = Quests.fromId(uniqueId);
if (!quest.isPresent())
{
QuestDaemon.log("Tried to load active quest that doesn't exist: " + uniqueId);
continue;
}
_activeQuests.add(quest.get());
}
}
}
catch (Exception e)
{
QuestDaemon.log(
"Exception encountered while loading last updated quests: " + e.getMessage());
QuestDaemon.log(e);
_currentDate = LocalDate.now(EST_TIMEZONE);
}
}
@Override
public void run()
{
try
{
while (_alive)
{
// purge recently selected quests repo of expired entries
_recentlySelectedQuestsRepo.clean();
LocalDate now = LocalDate.now(EST_TIMEZONE);
// check if date has changed; if so we need to choose new quests
if (_currentDate.isBefore(now) || _activeQuests.isEmpty())
{
QuestDaemon.log("Updating active quests...");
_currentDate = now;
// select new quests
selectRandomQuests();
// publish new quests
publishActiveQuests(PubSubChannels.QUEST_SUPPLIER_CHANNEL);
QuestDaemon.log("Done updating active quests.");
}
// take a small break, important so CPU isn't constantly running
Thread.sleep(SLEEP_MILLIS);
}
}
catch (InterruptedException e)
{
QuestDaemon.log("Exception encountered updating active quests repo: " + e.getMessage());
QuestDaemon.log(e);
}
}
private void publishActiveQuests(String channel)
{
QuestDaemon.log("publishing active quests to channel: " + channel);
QuestDaemon.log("Active quests: " + serialize(_activeQuests));
_pubSub.publish(channel,
serialize(_activeQuests));
}
/**
* Called on shutdown of this service. Writes the date quests were last updated to a file, so
* this service will know whether to update them or not on the next startup. This is all that's
* needed to keep active quests in a sane state because they are stored in redis.
*/
public void onShutdown()
{
_alive = false;
try
{
File file = new File(LAST_UPDATE_FILE);
if (!file.exists())
{
file.createNewFile();
}
List<String> lines = new ArrayList<>();
// add active quests date
lines.add(_currentDate.toString());
// add currently active quests
_activeQuests.stream().map(Quest::getDataId).forEach(lines::add);
Files.write(Paths.get(file.getAbsolutePath()), lines);
}
catch (IOException e)
{
QuestDaemon.log("Exception encountered saving " + LAST_UPDATE_FILE + " file: "
+ e.getMessage());
QuestDaemon.log(e);
}
}
protected void clearActiveQuests()
{
_activeQuests.clear();
}
protected void clearRecentlyActiveQuests()
{
_recentlySelectedQuestsRepo.getElements()
.forEach(_recentlySelectedQuestsRepo::removeElement);
}
protected void displayActiveQuests()
{
QuestDaemon.log("Active quests:");
_activeQuests.forEach(q -> QuestDaemon.log(q.toString()));
}
private void selectRandomQuests()
{
if (!_activeQuests.isEmpty())
{
_activeQuests.clear();
}
while (_activeQuests.size() < NUM_ACTIVE_QUESTS && _activeQuests.size() < _quests.values().size())
{
Quest q = _quests.next();
// select random weighted quest, ignore those recently selected
if (!q.shouldRotate() || _activeQuests.contains(q)
|| _recentlySelectedQuestsRepo.elementExists(q.getDataId()))
{
// quest is already active or it's been active recently
continue;
}
// add active quest
_activeQuests.add(q);
QuestDaemon.log("Selected quest: " + q.getName());
// flag quest as recently selected
_recentlySelectedQuestsRepo.addElement(q, RECENT_QUESTS_EXPIRE_SECONDS);
}
}
private String serialize(Set<Quest> quests)
{
return QuestTypeSerializer.QUEST_GSON.toJson(quests, QuestTypeSerializer.QUEST_TYPE);
}
}