diff --git a/Havana-Server/src/main/java/org/alexdev/havana/Havana.java b/Havana-Server/src/main/java/org/alexdev/havana/Havana.java index d7b41c9..0fccdf1 100644 --- a/Havana-Server/src/main/java/org/alexdev/havana/Havana.java +++ b/Havana-Server/src/main/java/org/alexdev/havana/Havana.java @@ -8,6 +8,7 @@ import org.alexdev.havana.game.GameScheduler; import org.alexdev.havana.game.achievements.AchievementManager; import org.alexdev.havana.game.ads.AdManager; 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.commands.CommandManager; import org.alexdev.havana.game.ecotron.EcotronManager; @@ -122,6 +123,7 @@ public class Havana { WalkwaysManager.getInstance(); ItemManager.getInstance(); CatalogueManager.getInstance(); + RareManager.getInstance(); EcotronManager.getInstance(); RoomModelManager.getInstance(); RoomManager.getInstance(); diff --git a/Havana-Server/src/main/java/org/alexdev/havana/dao/mysql/RareDao.java b/Havana-Server/src/main/java/org/alexdev/havana/dao/mysql/RareDao.java new file mode 100644 index 0000000..d94e384 --- /dev/null +++ b/Havana-Server/src/main/java/org/alexdev/havana/dao/mysql/RareDao.java @@ -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 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 getUsedRares() throws SQLException { + Map 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 getCurrentRare() throws SQLException { + Pair 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; + } +} + diff --git a/Havana-Server/src/main/java/org/alexdev/havana/game/GameScheduler.java b/Havana-Server/src/main/java/org/alexdev/havana/game/GameScheduler.java index 1f3debf..bde41ea 100644 --- a/Havana-Server/src/main/java/org/alexdev/havana/game/GameScheduler.java +++ b/Havana-Server/src/main/java/org/alexdev/havana/game/GameScheduler.java @@ -3,6 +3,7 @@ package org.alexdev.havana.game; import org.alexdev.havana.dao.mysql.ClubGiftDao; import org.alexdev.havana.dao.mysql.CurrencyDao; 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.club.ClubSubscription; import org.alexdev.havana.game.effects.Effect; @@ -238,7 +239,7 @@ public class GameScheduler implements Runnable { } CollectablesManager.getInstance().checkExpiries(); - + RareManager.getInstance().performRareManagerJob(this.tickRate); } catch (Exception ex) { Log.getErrorLogger().error("GameScheduler crashed: ", ex); } diff --git a/Havana-Server/src/main/java/org/alexdev/havana/game/catalogue/CataloguePage.java b/Havana-Server/src/main/java/org/alexdev/havana/game/catalogue/CataloguePage.java index 788466b..0b59df4 100644 --- a/Havana-Server/src/main/java/org/alexdev/havana/game/catalogue/CataloguePage.java +++ b/Havana-Server/src/main/java/org/alexdev/havana/game/catalogue/CataloguePage.java @@ -159,6 +159,10 @@ public class CataloguePage { return texts; } + public void setTexts(List texts) { + this.texts = texts; + } + public String getSeasonalStartDate() { return seasonalStartDate; } diff --git a/Havana-Server/src/main/java/org/alexdev/havana/game/catalogue/RareManager.java b/Havana-Server/src/main/java/org/alexdev/havana/game/catalogue/RareManager.java new file mode 100644 index 0000000..effd5a0 --- /dev/null +++ b/Havana-Server/src/main/java/org/alexdev/havana/game/catalogue/RareManager.java @@ -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 rareList; + private Map rareCost; + private Map 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 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 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 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)); + } +} diff --git a/Havana-Server/src/main/java/org/alexdev/havana/messages/incoming/catalogue/GET_CATALOGUE_PAGE.java b/Havana-Server/src/main/java/org/alexdev/havana/messages/incoming/catalogue/GET_CATALOGUE_PAGE.java index 1cd4616..315348e 100644 --- a/Havana-Server/src/main/java/org/alexdev/havana/messages/incoming/catalogue/GET_CATALOGUE_PAGE.java +++ b/Havana-Server/src/main/java/org/alexdev/havana/messages/incoming/catalogue/GET_CATALOGUE_PAGE.java @@ -3,14 +3,18 @@ package org.alexdev.havana.messages.incoming.catalogue; import org.alexdev.havana.game.catalogue.CatalogueItem; import org.alexdev.havana.game.catalogue.CatalogueManager; 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.player.Player; import org.alexdev.havana.messages.outgoing.catalogue.CATALOGUE_PAGE; import org.alexdev.havana.messages.types.MessageEvent; import org.alexdev.havana.server.netty.streams.NettyRequest; +import org.alexdev.havana.util.DateUtil; import org.alexdev.havana.util.config.GameConfiguration; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; public class GET_CATALOGUE_PAGE implements MessageEvent { @Override @@ -34,12 +38,40 @@ public class GET_CATALOGUE_PAGE implements MessageEvent { } if (player.getDetails().getRank().getRankId() >= cataloguePage.getMinRole().getRankId()) { - if (GameConfiguration.getInstance().getInteger("rare.cycle.page.id") == cataloguePage.getId()) { + List 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 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")) { cataloguePage.setLayout("pixelrent"); } else { cataloguePage.setLayout("cars"); } + */ } if (CollectablesManager.getInstance().getCollectableDataByPage(cataloguePage.getId()) != null) { @@ -58,7 +90,7 @@ public class GET_CATALOGUE_PAGE implements MessageEvent { } } - List catalogueItemList = CatalogueManager.getInstance().getCataloguePageItems(cataloguePage.getId(), false); + player.send(new CATALOGUE_PAGE(cataloguePage, catalogueItemList)); } } diff --git a/Havana-Server/src/main/java/org/alexdev/havana/messages/incoming/catalogue/GRPC.java b/Havana-Server/src/main/java/org/alexdev/havana/messages/incoming/catalogue/GRPC.java index d0496cf..7881c2e 100644 --- a/Havana-Server/src/main/java/org/alexdev/havana/messages/incoming/catalogue/GRPC.java +++ b/Havana-Server/src/main/java/org/alexdev/havana/messages/incoming/catalogue/GRPC.java @@ -3,10 +3,7 @@ package org.alexdev.havana.messages.incoming.catalogue; import org.alexdev.havana.dao.mysql.CurrencyDao; import org.alexdev.havana.dao.mysql.PlayerDao; import org.alexdev.havana.dao.mysql.TransactionDao; -import org.alexdev.havana.game.catalogue.CatalogueItem; -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.*; import org.alexdev.havana.game.catalogue.collectables.CollectablesManager; import org.alexdev.havana.game.fuserights.Fuseright; import org.alexdev.havana.game.item.Item; @@ -67,7 +64,7 @@ public class GRPC implements MessageEvent { item = seasonalItem; } else { // 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); 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 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 (CollectablesManager.getInstance().isCollectable(item)) { priceCoins = CollectablesManager.getInstance().getCollectableDataByItem(item.getId()).getActiveItem().getPriceCoins(); diff --git a/Havana-Server/src/main/java/org/alexdev/havana/util/config/writer/GameConfigWriter.java b/Havana-Server/src/main/java/org/alexdev/havana/util/config/writer/GameConfigWriter.java index 6b4263a..44463ec 100644 --- a/Havana-Server/src/main/java/org/alexdev/havana/util/config/writer/GameConfigWriter.java +++ b/Havana-Server/src/main/java/org/alexdev/havana/util/config/writer/GameConfigWriter.java @@ -85,6 +85,21 @@ public class GameConfigWriter implements ConfigWriter { config.put("events.category.count", "11"); config.put("events.expiry.minutes", "120"); + config.put("rare.cycle.page.text", "Okay this thing is fucking epic!

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.interval", "30"); config.put("club.gift.present.label", "You have just received your monthly club gift!");