Convert questmanager to read quests from google sheets

This commit is contained in:
Kenny 2017-05-05 15:22:37 -04:00
parent 87c01f994c
commit 54858e14ee
10 changed files with 272 additions and 99 deletions

View File

@ -32,6 +32,11 @@
<artifactId>mineplex-serverdata</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mineplex-questmanager</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>

View File

@ -7,7 +7,7 @@
<version>dev-SNAPSHOT</version>
</parent>
<artifactId>mineplex-questmanager</artifactId>
<name>Mineplex.QuestManager</name>
<name>Mineplex.questmanager</name>
<description>A centralized service that selects daily quests</description>
<properties>

View File

@ -0,0 +1,44 @@
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;
public BaseQuest(int uniqueId, String name, QuestRarity rarity)
{
_uniqueId = uniqueId;
_name = name;
_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 + "";
}
}

View File

@ -1,69 +1,71 @@
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;
/**
* A centralized list of {@link Quest}s.
* Provides access to all quests.
* <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.
* Loads quests from a google sheets json file.
*/
public enum Quests implements Quest
public class Quests
{
// 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 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 RARITY_COLUMN = 9;
;
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;
}
public static final Set<Quest> QUESTS;
@Override
public String getName()
static
{
return _name;
}
ImmutableSet.Builder<Quest> builder = ImmutableSet.<Quest>builder();
Map<String, List<List<String>>> sheets = UtilGoogleSheet.getSheetData(ALL_QUESTS_FILE);
@Override
public QuestRarity getRarity()
{
return _rarity;
}
List<List<String>> rows = sheets.getOrDefault(QUEST_SHEET_KEY, Collections.emptyList());
@Override
public int getUniqueId()
{
return _uniqueId;
}
@Override
public String getDataId()
{
return String.valueOf(_uniqueId);
}
public static Quest fromId(int id)
{
for (Quest q : values())
// get each row of spreadsheet, start at 1 since row 0 contains headers
for (int i = 1; i < rows.size(); i++)
{
if (q.getUniqueId() == id)
List<String> row = rows.get(i);
// attempt to parse quest data we need
try
{
return q;
int uniqueId = Integer.parseInt(row.get(UNIQUE_ID_COLUMN));
String name = row.get(NAME_COLUMN);
QuestRarity rarity = QuestRarity.valueOf(row.get(RARITY_COLUMN).toUpperCase());
Quest quest = new BaseQuest(uniqueId, name, rarity);
builder.add(quest);
}
catch (Exception e)
{
QuestDaemon.log("Exception encountered while parsing quest sheet row: " + row + ", "
+ e.getMessage());
e.printStackTrace();
}
}
return null;
QUESTS = builder.build();
}
}
public static Optional<Quest> fromId(int uniqueId)
{
return QUESTS.stream().filter(quest -> quest.getUniqueId() == uniqueId).findFirst();
}
}

View File

@ -12,7 +12,7 @@ import mineplex.serverdata.servers.ConnectionData;
*/
public class QuestRedisDataRepository extends RedisDataRepository<Quest>
{
public QuestRedisDataRepository(ConnectionData writeConn, ConnectionData readConn, Region region,
String elementLabel)
{
@ -22,7 +22,18 @@ public class QuestRedisDataRepository extends RedisDataRepository<Quest>
@Override
protected Quest deserialize(String json)
{
return Quests.fromId(Integer.parseInt(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

@ -3,6 +3,7 @@ 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;
@ -28,6 +29,7 @@ public class QuestTypeDeserialiazer implements JsonDeserializer<Set<Quest>>
{
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,5 +1,7 @@
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;
@ -8,13 +10,15 @@ 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 = new TreeMap<Double, E>();
private final NavigableMap<Double, E> _map = Collections.synchronizedNavigableMap(new TreeMap<Double, E>());
private final Random _random;
private double total = 0;
@ -50,4 +54,9 @@ public class RandomCollection<E>
double value = _random.nextDouble() * total;
return _map.ceilingEntry(value).getValue();
}
public Collection<E> values()
{
return _map.values();
}
}

View File

@ -0,0 +1,77 @@
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.
* @author Kenny
*
*/
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

@ -74,9 +74,8 @@ public class QuestDaemon
{
log("Starting QuestDaemon...");
_questManager = new QuestManager(
new PubSubRouter(new PubSubJedisClient(ServerManager.getMasterConnection(),
ServerManager.getSlaveConnection())));
_questManager = new QuestManager(new PubSubRouter(new PubSubJedisClient(
ServerManager.getMasterConnection(), ServerManager.getSlaveConnection())));
_questManager.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> _questManager.onShutdown()));
@ -94,10 +93,12 @@ public class QuestDaemon
{
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("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"))
else if (command.contains("stop"))
{
stopCommand();
}
@ -107,7 +108,7 @@ public class QuestDaemon
}
else if (command.contains("clearrecentquests"))
{
clearRecentQuestsCommand();
}
}
catch (Throwable t)
@ -133,6 +134,13 @@ public class QuestDaemon
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.");
}
public static void log(String message)
{
log(message, false);

View File

@ -7,7 +7,6 @@ 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;
@ -37,15 +36,16 @@ import mineplex.serverdata.servers.ServerManager;
public class QuestManager extends Thread
{
private static final int RECENT_QUESTS_EXPIRE_SECONDS = (int) TimeUnit.DAYS.toSeconds(5);
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<>());
@ -58,7 +58,7 @@ public class QuestManager extends Thread
// 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;
@ -67,21 +67,21 @@ public class QuestManager extends Thread
_pubSub = pubSub;
_recentlySelectedQuestsRepo = new QuestRedisDataRepository(
ServerManager.getMasterConnection(),
ServerManager.getSlaveConnection(), Region.currentRegion(), "recently-selected-quests");
ServerManager.getMasterConnection(), ServerManager.getSlaveConnection(),
Region.currentRegion(), "recently-selected-quests");
_quests.addAll(Arrays.stream(Quests.values())
.collect(Collectors.toMap(q -> q, q -> q.getRarity().getWeight())));
_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(((Quests) quest).name()));
_activeQuests.forEach(quest -> QuestDaemon.log(quest.getName()));
}
}
/**
* Loads last set active quests & the date they were set to active from a flat file, if the file
* exists.
@ -92,7 +92,7 @@ public class QuestManager extends Thread
if (!file.exists())
{
_currentDate = LocalDate.now(EST_TIMEZONE);
return;
}
@ -105,12 +105,13 @@ public class QuestManager extends Thread
{
for (int i = 1; i < lines.size(); i++)
{
Quest quest = Quests.fromId(Integer.parseInt(lines.get(i)));
int uniqueId = Integer.parseInt(lines.get(i));
Quest quest = getById(uniqueId);
_activeQuests.add(quest);
}
}
}
catch (IOException e)
catch (Exception e)
{
QuestDaemon.log(
"Exception encountered while loading last updated quests: " + e.getMessage());
@ -133,22 +134,24 @@ public class QuestManager extends Thread
{
QuestDaemon.log("Updating active quests...");
_currentDate = now;
// select new quests
selectRandomQuests();
// publish new quests
_pubSub.publish(PubSubChannels.QUEST_SUPPLIER_CHANNEL, serialize(_activeQuests));
_pubSub.publish(PubSubChannels.QUEST_SUPPLIER_CHANNEL,
serialize(_activeQuests));
}
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("Exception encountered updating active quests repo: " + e.getMessage());
QuestDaemon.log(e);
}
}
@ -158,10 +161,10 @@ public class QuestManager extends Thread
* 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()
public void onShutdown()
{
_alive = false;
try
{
File file = new File(LAST_UPDATE_FILE);
@ -169,20 +172,21 @@ public class QuestManager extends Thread
{
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("Exception encountered saving " + LAST_UPDATE_FILE + " file: "
+ e.getMessage());
QuestDaemon.log(e);
}
}
@ -192,6 +196,17 @@ public class QuestManager extends Thread
_activeQuests.clear();
}
public void clearRecentlyActiveQuests()
{
_recentlySelectedQuestsRepo.clean();
}
public Quest getById(int uniqueId)
{
return _quests.values().stream().filter(q -> q.getUniqueId() == uniqueId).findFirst()
.orElse(null);
}
private void selectRandomQuests()
{
if (!_activeQuests.isEmpty())
@ -199,27 +214,27 @@ public class QuestManager extends Thread
_activeQuests.clear();
}
while (_activeQuests.size() < NUM_ACTIVE_QUESTS)
while (_activeQuests.size() < NUM_ACTIVE_QUESTS && _activeQuests.size() < _quests.values().size())
{
Quest q = _quests.next();
// select random weighted quest, ignore those recently selected
while (_activeQuests.contains(q)
&& _recentlySelectedQuestsRepo.elementExists(q.getDataId()))
if (_activeQuests.contains(q)
|| _recentlySelectedQuestsRepo.elementExists(q.getDataId()))
{
// quest is already active or it's been active recently
q = _quests.next();
continue;
}
// add active quest
_activeQuests.add(q);
QuestDaemon.log("Selected quest: " + ((Quests) q).name());
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);