Remove the QuestManager code from PC
This commit is contained in:
parent
4fb6608c3f
commit
e45deb127c
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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:";
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()) + "] ";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user