Port questmanager service from PE
This commit is contained in:
parent
ff13d07a7c
commit
87c01f994c
69
Plugins/mineplex-questmanager/pom.xml
Normal file
69
Plugins/mineplex-questmanager/pom.xml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<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>18.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>
|
@ -0,0 +1,93 @@
|
|||||||
|
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 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;
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package mineplex.quest.common;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package mineplex.quest.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How rare a quest is. In other words, how often this quest should be chosen.
|
||||||
|
*/
|
||||||
|
public enum QuestRarity
|
||||||
|
{
|
||||||
|
// TODO fill in actual weights
|
||||||
|
COMMON(0.5),
|
||||||
|
RARE(0.3),
|
||||||
|
LEGENDARY(0.1)
|
||||||
|
;
|
||||||
|
|
||||||
|
private final double _weight;
|
||||||
|
|
||||||
|
private QuestRarity(double weight)
|
||||||
|
{
|
||||||
|
_weight = weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWeight()
|
||||||
|
{
|
||||||
|
return _weight;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package mineplex.quest.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A centralized list of {@link Quest}s.
|
||||||
|
* <p>
|
||||||
|
* When adding new quests they should be given an enum field here & then added to the minecraft
|
||||||
|
* server plugin in the form of a "tracker" that watches progress per user.
|
||||||
|
*/
|
||||||
|
public enum Quests implements Quest
|
||||||
|
{
|
||||||
|
// TODO add actual quests
|
||||||
|
|
||||||
|
EXAMPLE_QUEST_1("1", 0, QuestRarity.COMMON),
|
||||||
|
EXAMPLE_QUEST_2("2", 1, QuestRarity.COMMON),
|
||||||
|
EXAMPLE_QUEST_3("3", 2, QuestRarity.RARE),
|
||||||
|
EXAMPLE_QUEST_4("4", 3, QuestRarity.LEGENDARY),
|
||||||
|
EXAMPLE_QUEST_5("5", 4, QuestRarity.COMMON),
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
private final String _name;
|
||||||
|
private final int _uniqueId;
|
||||||
|
private final QuestRarity _rarity;
|
||||||
|
|
||||||
|
Quests(String name, int uniqueId, QuestRarity rarity)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_uniqueId = uniqueId;
|
||||||
|
_rarity = rarity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return _name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QuestRarity getRarity()
|
||||||
|
{
|
||||||
|
return _rarity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getUniqueId()
|
||||||
|
{
|
||||||
|
return _uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDataId()
|
||||||
|
{
|
||||||
|
return String.valueOf(_uniqueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Quest fromId(int id)
|
||||||
|
{
|
||||||
|
for (Quest q : values())
|
||||||
|
{
|
||||||
|
if (q.getUniqueId() == id)
|
||||||
|
{
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
return Quests.fromId(Integer.parseInt(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package mineplex.quest.common.redis;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
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)))
|
||||||
|
.collect(Collectors.toCollection(HashSet::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package mineplex.quest.common.util;
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param <E> The generic type parameter of the elements.
|
||||||
|
*/
|
||||||
|
public class RandomCollection<E>
|
||||||
|
{
|
||||||
|
|
||||||
|
private final NavigableMap<Double, E> _map = 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();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
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.");
|
||||||
|
}
|
||||||
|
if (command.contains("stop"))
|
||||||
|
{
|
||||||
|
stopCommand();
|
||||||
|
}
|
||||||
|
else if (command.contains("clearactivequests"))
|
||||||
|
{
|
||||||
|
clearQuestsCommand();
|
||||||
|
}
|
||||||
|
else if (command.contains("clearrecentquests"))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Throwable t)
|
||||||
|
{
|
||||||
|
log("Exception encountered while executing command " + command + ": "
|
||||||
|
+ t.getMessage());
|
||||||
|
log(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopCommand() throws Exception
|
||||||
|
{
|
||||||
|
log("Shutting down QuestDaemon...");
|
||||||
|
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearQuestsCommand()
|
||||||
|
{
|
||||||
|
_questManager.clearActiveQuests();
|
||||||
|
|
||||||
|
log("Cleared active quests. New ones will be selected on this instance's next iteration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) + "] ";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,227 @@
|
|||||||
|
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.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
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(Arrays.stream(Quests.values())
|
||||||
|
.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(((Quests) quest).name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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++)
|
||||||
|
{
|
||||||
|
Quest quest = Quests.fromId(Integer.parseInt(lines.get(i)));
|
||||||
|
_activeQuests.add(quest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException 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)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
_pubSub.publish(PubSubChannels.QUEST_SUPPLIER_CHANNEL, serialize(_activeQuests));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearActiveQuests()
|
||||||
|
{
|
||||||
|
_activeQuests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectRandomQuests()
|
||||||
|
{
|
||||||
|
if (!_activeQuests.isEmpty())
|
||||||
|
{
|
||||||
|
_activeQuests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_activeQuests.size() < NUM_ACTIVE_QUESTS)
|
||||||
|
{
|
||||||
|
Quest q = _quests.next();
|
||||||
|
// select random weighted quest, ignore those recently selected
|
||||||
|
while (_activeQuests.contains(q)
|
||||||
|
&& _recentlySelectedQuestsRepo.elementExists(q.getDataId()))
|
||||||
|
{
|
||||||
|
// quest is already active or it's been active recently
|
||||||
|
q = _quests.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// add active quest
|
||||||
|
_activeQuests.add(q);
|
||||||
|
|
||||||
|
QuestDaemon.log("Selected quest: " + ((Quests) q).name());
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,7 @@
|
|||||||
<module>mavericks-review-hub</module>
|
<module>mavericks-review-hub</module>
|
||||||
<module>mineplex-game-gemhunters</module>
|
<module>mineplex-game-gemhunters</module>
|
||||||
<module>mineplex-google-sheets</module>
|
<module>mineplex-google-sheets</module>
|
||||||
|
<module>mineplex-questmanager</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
|
Loading…
Reference in New Issue
Block a user