Convert questmanager to read quests from google sheets
This commit is contained in:
parent
87c01f994c
commit
54858e14ee
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 + "";
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user