1
Fork 0
mirror of https://github.com/Quackster/Havana.git synced 2025-07-05 06:07:46 +00:00

feat: port kepler rare manager

This commit is contained in:
jtieri 2023-04-13 17:52:22 -05:00
parent 2979285515
commit 15bb3843ba
8 changed files with 455 additions and 8 deletions

View file

@ -8,6 +8,7 @@ import org.alexdev.havana.game.GameScheduler;
import org.alexdev.havana.game.achievements.AchievementManager; import org.alexdev.havana.game.achievements.AchievementManager;
import org.alexdev.havana.game.ads.AdManager; import org.alexdev.havana.game.ads.AdManager;
import org.alexdev.havana.game.catalogue.CatalogueManager; import org.alexdev.havana.game.catalogue.CatalogueManager;
import org.alexdev.havana.game.catalogue.RareManager;
import org.alexdev.havana.game.catalogue.collectables.CollectablesManager; import org.alexdev.havana.game.catalogue.collectables.CollectablesManager;
import org.alexdev.havana.game.commands.CommandManager; import org.alexdev.havana.game.commands.CommandManager;
import org.alexdev.havana.game.ecotron.EcotronManager; import org.alexdev.havana.game.ecotron.EcotronManager;
@ -122,6 +123,7 @@ public class Havana {
WalkwaysManager.getInstance(); WalkwaysManager.getInstance();
ItemManager.getInstance(); ItemManager.getInstance();
CatalogueManager.getInstance(); CatalogueManager.getInstance();
RareManager.getInstance();
EcotronManager.getInstance(); EcotronManager.getInstance();
RoomModelManager.getInstance(); RoomModelManager.getInstance();
RoomManager.getInstance(); RoomManager.getInstance();

View file

@ -0,0 +1,113 @@
package org.alexdev.havana.dao.mysql;
import org.alexdev.havana.dao.Storage;
import org.apache.commons.lang3.tuple.Pair;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class RareDao {
public static void addRare(String sprite, long reuseTime) throws SQLException {
Connection sqlConnection = null;
PreparedStatement preparedStatement = null;
try {
sqlConnection = Storage.getStorage().getConnection();
preparedStatement = Storage.getStorage().prepare("INSERT INTO rare_cycle (sale_code, reuse_time) VALUES (?, ?)", sqlConnection);
preparedStatement.setString(1, sprite);
preparedStatement.setLong(2, reuseTime);
preparedStatement.execute();
} catch (Exception e) {
Storage.logError(e);
throw e;
} finally {
Storage.closeSilently(preparedStatement);
Storage.closeSilently(sqlConnection);
}
}
public static void removeRares(List<String> sprites) throws SQLException {
Connection sqlConnection = null;
PreparedStatement preparedStatement = null;
try {
sqlConnection = Storage.getStorage().getConnection();
preparedStatement = Storage.getStorage().prepare("DELETE FROM rare_cycle WHERE sale_code = ?", sqlConnection);
sqlConnection.setAutoCommit(false);
for (String sprite : sprites) {
preparedStatement.setString(1, sprite);
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
sqlConnection.setAutoCommit(true);
} catch (Exception e) {
Storage.logError(e);
throw e;
} finally {
Storage.closeSilently(preparedStatement);
Storage.closeSilently(sqlConnection);
}
}
public static Map<String, Long> getUsedRares() throws SQLException {
Map<String, Long> rares = new LinkedHashMap<>();
Connection sqlConnection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
String itemSaleCode = null;
try {
sqlConnection = Storage.getStorage().getConnection();
preparedStatement = Storage.getStorage().prepare("SELECT sale_code, reuse_time FROM rare_cycle ORDER BY reuse_time DESC", sqlConnection);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
rares.put(resultSet.getString("sale_code"), resultSet.getLong("reuse_time"));
}
} catch (Exception e) {
Storage.logError(e);
throw e;
} finally {
Storage.closeSilently(preparedStatement);
Storage.closeSilently(sqlConnection);
}
return rares;
}
public static Pair<String, Long> getCurrentRare() throws SQLException {
Pair<String, Long> itemData = null;
Connection sqlConnection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
sqlConnection = Storage.getStorage().getConnection();
preparedStatement = Storage.getStorage().prepare("SELECT sale_code, reuse_time FROM rare_cycle ORDER BY reuse_time DESC LIMIT 1", sqlConnection);
resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
itemData = Pair.of(resultSet.getString("sale_code"), resultSet.getLong("reuse_time"));
}
} catch (Exception e) {
Storage.logError(e);
throw e;
} finally {
Storage.closeSilently(preparedStatement);
Storage.closeSilently(sqlConnection);
}
return itemData;
}
}

View file

@ -3,6 +3,7 @@ package org.alexdev.havana.game;
import org.alexdev.havana.dao.mysql.ClubGiftDao; import org.alexdev.havana.dao.mysql.ClubGiftDao;
import org.alexdev.havana.dao.mysql.CurrencyDao; import org.alexdev.havana.dao.mysql.CurrencyDao;
import org.alexdev.havana.dao.mysql.EffectDao; import org.alexdev.havana.dao.mysql.EffectDao;
import org.alexdev.havana.game.catalogue.RareManager;
import org.alexdev.havana.game.catalogue.collectables.CollectablesManager; import org.alexdev.havana.game.catalogue.collectables.CollectablesManager;
import org.alexdev.havana.game.club.ClubSubscription; import org.alexdev.havana.game.club.ClubSubscription;
import org.alexdev.havana.game.effects.Effect; import org.alexdev.havana.game.effects.Effect;
@ -238,7 +239,7 @@ public class GameScheduler implements Runnable {
} }
CollectablesManager.getInstance().checkExpiries(); CollectablesManager.getInstance().checkExpiries();
RareManager.getInstance().performRareManagerJob(this.tickRate);
} catch (Exception ex) { } catch (Exception ex) {
Log.getErrorLogger().error("GameScheduler crashed: ", ex); Log.getErrorLogger().error("GameScheduler crashed: ", ex);
} }

View file

@ -159,6 +159,10 @@ public class CataloguePage {
return texts; return texts;
} }
public void setTexts(List<String> texts) {
this.texts = texts;
}
public String getSeasonalStartDate() { public String getSeasonalStartDate() {
return seasonalStartDate; return seasonalStartDate;
} }

View file

@ -0,0 +1,275 @@
package org.alexdev.havana.game.catalogue;
import org.alexdev.havana.dao.Storage;
import org.alexdev.havana.dao.mysql.RareDao;
import org.alexdev.havana.dao.mysql.SettingsDao;
import org.alexdev.havana.util.DateUtil;
import org.alexdev.havana.util.config.GameConfiguration;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class RareManager {
private static RareManager instance;
private final String RARE_TICK_SETTING = "rare.cycle.tick.time";
private LinkedList<CatalogueItem> rareList;
private Map<CatalogueItem, Integer> rareCost;
private Map<String, Long> daysSinceUsed;
private CatalogueItem currentRare;
private Long currentRareTime;
private AtomicLong tickTime;
public RareManager() {
this.rareCost = new HashMap<>();
this.tickTime = new AtomicLong(GameConfiguration.getInstance().getLong(RARE_TICK_SETTING));
String[] hourData = GameConfiguration.getInstance().getString("rare.cycle.pages").split("\\|");
for (String numbers : hourData) {
int cataloguePage = Integer.parseInt(numbers.split(",")[0]);
int hoursRequired = Integer.parseInt(numbers.split(",")[1]);
//System.out.printf("Catalogue Page: %d \n", cataloguePage);
//System.out.printf("Hours Required: %d \n", hoursRequired);
if (hoursRequired > 0) {
for (CatalogueItem item : CatalogueManager.getInstance().getCataloguePageItems(cataloguePage, true)) {
//System.out.printf("Rare Item Name: %s \n", item.getDefinition().getName());
//System.out.printf("Rare Item Cost: %d \n", item.getPriceCoins());
var costInHours = getHandoutAmountInHours(hoursRequired);
//System.out.printf("Rare Cost Hours: %d \n", costInHours);
//System.out.println("-------------------------------------------");
this.rareCost.put(item, costInHours);
}
}
//System.out.println("--------------------");
}
try {
this.daysSinceUsed = RareDao.getUsedRares();
if (this.daysSinceUsed.size() > 0) {
var currentItemData = RareDao.getCurrentRare();
this.currentRare = CatalogueManager.getInstance().getCatalogueItem(currentItemData.getKey());
this.currentRareTime = currentItemData.getValue(); // Get the active item
}
this.loadRares();
// If there was no current rare, or the current rare time ran out, then cycle to the next rare
if (this.currentRare == null) {
this.selectNewRare();
}
} catch (Exception ex) {
Storage.logError(ex);
}
}
/**
* Finds all rares that can't be accessed normally by the user, and then shuffles the list.
*/
private void loadRares() {
this.rareList = new LinkedList<>();
String[] hourData = GameConfiguration.getInstance().getString("rare.cycle.pages").split("\\|");
List<Integer> rarePages = new LinkedList<>();
for (String numbers : hourData) {
int cataloguePage = Integer.parseInt(numbers.split(",")[0]);
rarePages.add(cataloguePage);
}
for (Integer pageNumber : rarePages) {
var cataloguePage = CatalogueManager.getInstance().getCataloguePage(pageNumber);
// Skip pages where normal users can access
if (!(cataloguePage.getMinRole().getRankId() > 1)) {
continue;
}
// TODO: What we should really do is create one or two pages of rares used in rare cycler
// then we can just check for this page number and only shuffle rares from these pages.
// Search in rares pages only
boolean skipPage = true;
for (String image : cataloguePage.getImages()) {
if (image.equals("catalog_rares_headline1")) {
skipPage = false;
break;
}
}
if (!cataloguePage.getLayout().equals("default_3x3") || skipPage) {
continue;
}
this.rareList.addAll(CatalogueManager.getInstance().getCataloguePageItems(cataloguePage.getId(), true));
}
Collections.shuffle(this.rareList);
}
/**
* Selects a new rare, adds it to the database so it can only be selected once every X interval defined (default is 3 days).
*/
public void selectNewRare() throws SQLException {
TimeUnit reuseTimeUnit = TimeUnit.valueOf(GameConfiguration.getInstance().getString("rare.cycle.reuse.timeunit"));
long interval = reuseTimeUnit.toSeconds(GameConfiguration.getInstance().getInteger("rare.cycle.reuse.interval"));
List<String> toRemove = new ArrayList<>();
// Remove expired rares
for (var kvp : this.daysSinceUsed.entrySet()) {
if (DateUtil.getCurrentTimeSeconds() > kvp.getValue()) {
toRemove.add(kvp.getKey());
}
}
for (var sprite : toRemove) {
this.daysSinceUsed.remove(sprite);
}
RareDao.removeRares(toRemove);
// If the rare list has ran out, reload it.
if (this.rareList.isEmpty()) {
this.loadRares();
}
CatalogueItem rare = this.rareList.pollFirst(); // Select the rare from the rare list
if (rare != null) {
// If the rare is in the expired list, search for another rare
if (this.daysSinceUsed.containsKey(rare.getDefinition().getSprite())) {
this.currentRare = null; // Set to null in case we can't find one, so it can default back to the default catalogue item set in database
if (this.rareList.size() > 0) {
this.selectNewRare();
}
return;
}
this.currentRare = rare;
// Handle override by using "rare.cycle.reuse.CATALOGUE_SALE_CODE.timeunit" and "rare.cycle.reuse.CATALOGUE_SALE_CODE.interval"
String overrideUnit = GameConfiguration.getInstance().getString("rare.cycle.reuse." + rare.getSaleCode() + ".timeunit", null);
if (overrideUnit != null) {
reuseTimeUnit = TimeUnit.valueOf(overrideUnit);
interval = reuseTimeUnit.toSeconds(GameConfiguration.getInstance().getInteger("rare.cycle.reuse." + rare.getSaleCode() + ".interval"));
}
// Add rare to expiry table so it can't be used for a certain X number of days
this.daysSinceUsed.put(rare.getDefinition().getSprite(), DateUtil.getCurrentTimeSeconds() + interval);
RareDao.removeRares(List.of(rare.getDefinition().getSprite()));
RareDao.addRare(rare.getDefinition().getSprite(), DateUtil.getCurrentTimeSeconds() + interval);
this.tickTime.set(0);
this.saveTick();
}
}
/**
* Get the credit amount handout but in hours.
*
* @param hours the hours to select for
* @return the amount of rareCost
*/
public int getHandoutAmountInHours(int hours) {
TimeUnit unit = TimeUnit.valueOf(GameConfiguration.getInstance().getString("credits.scheduler.timeunit"));
long interval = unit.toMinutes(GameConfiguration.getInstance().getInteger("credits.scheduler.interval"));
long minutesInHour = 60;
long minutes = minutesInHour / interval;
return (int) ((hours * minutes) * GameConfiguration.getInstance().getInteger("credits.scheduler.amount"));
}
/**
* Tick manager for checking expiry of the rare on sale.
*
* @param tickTime the global tick counter instance
* @throws SQLException for when selectNewRare() fails
*/
public void performRareManagerJob(AtomicLong tickTime) throws SQLException {
// Rare cycle management
TimeUnit rareManagerUnit = TimeUnit.valueOf(GameConfiguration.getInstance().getString("rare.cycle.refresh.timeunit"));
long interval = rareManagerUnit.toSeconds(GameConfiguration.getInstance().getInteger("rare.cycle.refresh.interval"));
RareManager.getInstance().getTick().incrementAndGet();
// Save tick time every 60 seconds...
if (tickTime.get() % 60 == 0) {
RareManager.getInstance().saveTick();
}
// Select new rare
if (RareManager.getInstance().getTick().get() >= interval) {
RareManager.getInstance().selectNewRare();
}
}
/**
* Remove the colour tag from the sprite name. eg pillow*1 to pillow, used for
* comparing the same items which are just different colours.
*
* @param sprite the sprite to remove the colour tag from
* @return the new sprite
*/
private String stripColor(String sprite) {
return sprite.contains("*") ? sprite.split("\\*")[0] : sprite;
}
/**
* Get the current random rare
* @return the random rare
*/
public CatalogueItem getCurrentRare() {
return currentRare;
}
/**
* Get the rares costs.
*
* @return the map of rares costs
*/
public Map<CatalogueItem, Integer> getRareCost() {
return rareCost;
}
/**
* Get the {@link RareManager} instance
*
* @return the rare manager instance
*/
public static RareManager getInstance() {
if (instance == null) {
instance = new RareManager();
}
return instance;
}
/**
* Get the current tick.
* @return the tick time
*/
public AtomicLong getTick() {
return tickTime;
}
/**
* Save the tick time to database
*/
public void saveTick() {
GameConfiguration.getInstance().getConfig().put(RARE_TICK_SETTING, String.valueOf(RareManager.getInstance().getTick().get()));
SettingsDao.updateSetting(RARE_TICK_SETTING, GameConfiguration.getInstance().getString(RARE_TICK_SETTING));
}
}

View file

@ -3,14 +3,18 @@ package org.alexdev.havana.messages.incoming.catalogue;
import org.alexdev.havana.game.catalogue.CatalogueItem; import org.alexdev.havana.game.catalogue.CatalogueItem;
import org.alexdev.havana.game.catalogue.CatalogueManager; import org.alexdev.havana.game.catalogue.CatalogueManager;
import org.alexdev.havana.game.catalogue.CataloguePage; import org.alexdev.havana.game.catalogue.CataloguePage;
import org.alexdev.havana.game.catalogue.RareManager;
import org.alexdev.havana.game.catalogue.collectables.CollectablesManager; import org.alexdev.havana.game.catalogue.collectables.CollectablesManager;
import org.alexdev.havana.game.player.Player; import org.alexdev.havana.game.player.Player;
import org.alexdev.havana.messages.outgoing.catalogue.CATALOGUE_PAGE; import org.alexdev.havana.messages.outgoing.catalogue.CATALOGUE_PAGE;
import org.alexdev.havana.messages.types.MessageEvent; import org.alexdev.havana.messages.types.MessageEvent;
import org.alexdev.havana.server.netty.streams.NettyRequest; import org.alexdev.havana.server.netty.streams.NettyRequest;
import org.alexdev.havana.util.DateUtil;
import org.alexdev.havana.util.config.GameConfiguration; import org.alexdev.havana.util.config.GameConfiguration;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
public class GET_CATALOGUE_PAGE implements MessageEvent { public class GET_CATALOGUE_PAGE implements MessageEvent {
@Override @Override
@ -34,12 +38,40 @@ public class GET_CATALOGUE_PAGE implements MessageEvent {
} }
if (player.getDetails().getRank().getRankId() >= cataloguePage.getMinRole().getRankId()) { if (player.getDetails().getRank().getRankId() >= cataloguePage.getMinRole().getRankId()) {
if (GameConfiguration.getInstance().getInteger("rare.cycle.page.id") == cataloguePage.getId()) { List<CatalogueItem> catalogueItemList = CatalogueManager.getInstance().getCataloguePageItems(cataloguePage.getId(), false);
if (RareManager.getInstance().getCurrentRare() != null &&
GameConfiguration.getInstance().getInteger("rare.cycle.page.id") == cataloguePage.getId()) {
var currentRare = RareManager.getInstance().getCurrentRare();
var rareItem = currentRare.copy();
var cost = RareManager.getInstance().getRareCost().get(currentRare);
rareItem.setPriceCoins(cost);
catalogueItemList = List.of(rareItem);
TimeUnit rareManagerUnit = TimeUnit.valueOf(GameConfiguration.getInstance().getString("rare.cycle.refresh.timeunit"));
long interval = rareManagerUnit.toSeconds(GameConfiguration.getInstance().getInteger("rare.cycle.refresh.interval"));
long currentTick = RareManager.getInstance().getTick().get();
long timeUntil = interval - currentTick;
List<String> newTexts = new ArrayList<>();
newTexts.add(GameConfiguration.getInstance().getString("rare.cycle.page.text").replace("{rareCountdown}", DateUtil.getReadableSeconds(timeUntil)));
cataloguePage.setTexts(newTexts);
/*
This was already here in Havana before porting Keplers Rare Manager code.
This was still referencing the setting rare.cycle.page.id so this was an unreachable code path
Commenting this out should do nothing and is probably an artifact of rare manager code that
Quackster probably ripped out before releasing Havana.
if (GameConfiguration.getInstance().getBoolean("rare.cycle.pixels.only")) { if (GameConfiguration.getInstance().getBoolean("rare.cycle.pixels.only")) {
cataloguePage.setLayout("pixelrent"); cataloguePage.setLayout("pixelrent");
} else { } else {
cataloguePage.setLayout("cars"); cataloguePage.setLayout("cars");
} }
*/
} }
if (CollectablesManager.getInstance().getCollectableDataByPage(cataloguePage.getId()) != null) { if (CollectablesManager.getInstance().getCollectableDataByPage(cataloguePage.getId()) != null) {
@ -58,7 +90,7 @@ public class GET_CATALOGUE_PAGE implements MessageEvent {
} }
} }
List<CatalogueItem> catalogueItemList = CatalogueManager.getInstance().getCataloguePageItems(cataloguePage.getId(), false);
player.send(new CATALOGUE_PAGE(cataloguePage, catalogueItemList)); player.send(new CATALOGUE_PAGE(cataloguePage, catalogueItemList));
} }
} }

View file

@ -3,10 +3,7 @@ package org.alexdev.havana.messages.incoming.catalogue;
import org.alexdev.havana.dao.mysql.CurrencyDao; import org.alexdev.havana.dao.mysql.CurrencyDao;
import org.alexdev.havana.dao.mysql.PlayerDao; import org.alexdev.havana.dao.mysql.PlayerDao;
import org.alexdev.havana.dao.mysql.TransactionDao; import org.alexdev.havana.dao.mysql.TransactionDao;
import org.alexdev.havana.game.catalogue.CatalogueItem; import org.alexdev.havana.game.catalogue.*;
import org.alexdev.havana.game.catalogue.CatalogueManager;
import org.alexdev.havana.game.catalogue.CataloguePackage;
import org.alexdev.havana.game.catalogue.CataloguePage;
import org.alexdev.havana.game.catalogue.collectables.CollectablesManager; import org.alexdev.havana.game.catalogue.collectables.CollectablesManager;
import org.alexdev.havana.game.fuserights.Fuseright; import org.alexdev.havana.game.fuserights.Fuseright;
import org.alexdev.havana.game.item.Item; import org.alexdev.havana.game.item.Item;
@ -67,7 +64,7 @@ public class GRPC implements MessageEvent {
item = seasonalItem; item = seasonalItem;
} else { } else {
// If the item is not a buyable special rare, then check if they can actually buy it // If the item is not a buyable special rare, then check if they can actually buy it
if (!CollectablesManager.getInstance().isCollectable(item)) { if (!CollectablesManager.getInstance().isCollectable(item) || (RareManager.getInstance().getCurrentRare() != null && item != RareManager.getInstance().getCurrentRare())) {
CataloguePage page = CatalogueManager.getInstance().getCataloguePages().stream().filter(p -> finalItem.hasPage(p.getId())).findFirst().orElse(null); CataloguePage page = CatalogueManager.getInstance().getCataloguePages().stream().filter(p -> finalItem.hasPage(p.getId())).findFirst().orElse(null);
if (page == null) {// || pageStream.get().getMinRole().getRankId() > player.getDetails().getRank().getRankId()) { if (page == null) {// || pageStream.get().getMinRole().getRankId() > player.getDetails().getRank().getRankId()) {
@ -89,6 +86,14 @@ public class GRPC implements MessageEvent {
int priceCoins = item.getPriceCoins(); int priceCoins = item.getPriceCoins();
int pricePixels = item.getPricePixels(); int pricePixels = item.getPricePixels();
var currentRare = RareManager.getInstance().getCurrentRare();
if (currentRare != null && currentRare == item) {
if (!player.hasFuse(Fuseright.CREDITS)) {
priceCoins = RareManager.getInstance().getRareCost().get(currentRare);
}
}
if (!(player.getDetails().getRank().getRankId() >= PlayerRank.COMMUNITY_MANAGER.getRankId())) { if (!(player.getDetails().getRank().getRankId() >= PlayerRank.COMMUNITY_MANAGER.getRankId())) {
if (CollectablesManager.getInstance().isCollectable(item)) { if (CollectablesManager.getInstance().isCollectable(item)) {
priceCoins = CollectablesManager.getInstance().getCollectableDataByItem(item.getId()).getActiveItem().getPriceCoins(); priceCoins = CollectablesManager.getInstance().getCollectableDataByItem(item.getId()).getActiveItem().getPriceCoins();

View file

@ -85,6 +85,21 @@ public class GameConfigWriter implements ConfigWriter {
config.put("events.category.count", "11"); config.put("events.category.count", "11");
config.put("events.expiry.minutes", "120"); config.put("events.expiry.minutes", "120");
config.put("rare.cycle.page.text", "Okay this thing is fucking epic!<br><br>The time until the next rare is {rareCountdown}!");
config.put("rare.cycle.tick.time", "0");
config.put("rare.cycle.page.id", "143");
config.put("rare.cycle.refresh.timeunit", "DAYS");
config.put("rare.cycle.refresh.interval", "1");
config.put("rare.cycle.reuse.timeunit", "DAYS");
config.put("rare.cycle.reuse.interval", "7");
config.put("rare.cycle.reuse.throne.timeunit", "DAYS");
config.put("rare.cycle.reuse.throne.interval", "30");
// Catalogue pages for rare items, delimetered by pipe, first integer is page ID and second number is the amount of hours required for that rare to be affordable
config.put("rare.cycle.pages", "28,3|29,3|31,3|32,3|33,3|34,3|35,3|36,3|40,3|43,3|30,6|37,6|38,6|39,6|44,6");
config.put("club.gift.timeunit", "DAYS"); config.put("club.gift.timeunit", "DAYS");
config.put("club.gift.interval", "30"); config.put("club.gift.interval", "30");
config.put("club.gift.present.label", "You have just received your monthly club gift!"); config.put("club.gift.present.label", "You have just received your monthly club gift!");