/* This file is part of the OdinMS Maple Story Server Copyright (C) 2008 Patrick Huy Matthias Butz Jan Christian Meyer This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation version 3 as published by the Free Software Foundation. You may not use, modify or distribute this program under any other version of the GNU Affero General Public License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package server; import client.inventory.Equip; import client.inventory.InventoryType; import client.inventory.Item; import client.inventory.ItemFactory; import client.inventory.Pet; import config.YamlConfig; import constants.id.ItemId; import constants.inventory.ItemConstants; import net.jcip.annotations.GuardedBy; import net.server.Server; import provider.Data; import provider.DataProvider; import provider.DataProviderFactory; import provider.DataTool; import provider.wz.WZFiles; import tools.DatabaseConnection; import tools.Pair; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.HOURS; /* * @author Flav * @author Ponk */ public class CashShop { public static final int NX_CREDIT = 1; public static final int MAPLE_POINT = 2; public static final int NX_PREPAID = 4; private final int accountId; private final int characterId; private int nxCredit; private int maplePoint; private int nxPrepaid; private boolean opened; private ItemFactory factory; private final List inventory = new ArrayList<>(); private final List wishList = new ArrayList<>(); private int notes = 0; private final Lock lock = new ReentrantLock(); public CashShop(int accountId, int characterId, int jobType) throws SQLException { this.accountId = accountId; this.characterId = characterId; if (!YamlConfig.config.server.USE_JOINT_CASHSHOP_INVENTORY) { switch (jobType) { case 0: factory = ItemFactory.CASH_EXPLORER; break; case 1: factory = ItemFactory.CASH_CYGNUS; break; case 2: factory = ItemFactory.CASH_ARAN; break; } } else { factory = ItemFactory.CASH_OVERALL; } try (Connection con = DatabaseConnection.getConnection()) { try (PreparedStatement ps = con.prepareStatement("SELECT `nxCredit`, `maplePoint`, `nxPrepaid` FROM `accounts` WHERE `id` = ?")) { ps.setInt(1, accountId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { this.nxCredit = rs.getInt("nxCredit"); this.maplePoint = rs.getInt("maplePoint"); this.nxPrepaid = rs.getInt("nxPrepaid"); } } } for (Pair item : factory.loadItems(accountId, false)) { inventory.add(item.getLeft()); } try (PreparedStatement ps = con.prepareStatement("SELECT `sn` FROM `wishlists` WHERE `charid` = ?")) { ps.setInt(1, characterId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { wishList.add(rs.getInt("sn")); } } } } } public static class CashItem { private final int sn; private final int itemId; private final int price; private final long period; private final short count; private final boolean onSale; private CashItem(int sn, int itemId, int price, long period, short count, boolean onSale) { this.sn = sn; this.itemId = itemId; this.price = price; this.period = (period == 0 ? 90 : period); this.count = count; this.onSale = onSale; } public int getSN() { return sn; } public int getItemId() { return itemId; } public int getPrice() { return price; } public short getCount() { return count; } public boolean isOnSale() { return onSale; } public Item toItem() { Item item; int petid = -1; if (ItemConstants.isPet(itemId)) { petid = Pet.createPet(itemId); } if (ItemConstants.getInventoryType(itemId).equals(InventoryType.EQUIP)) { item = ItemInformationProvider.getInstance().getEquipById(itemId); } else { item = new Item(itemId, (byte) 0, count, petid); } if (ItemConstants.EXPIRING_ITEMS) { if (period == 1) { switch (itemId) { case ItemId.DROP_COUPON_2X_4H, ItemId.EXP_COUPON_2X_4H: // 4 Hour 2X coupons, the period is 1, but we don't want them to last a day. item.setExpiration(Server.getInstance().getCurrentTime() + HOURS.toMillis(4)); /* } else if(itemId == 5211047 || itemId == 5360014) { // 3 Hour 2X coupons, unused as of now item.setExpiration(Server.getInstance().getCurrentTime() + HOURS.toMillis(3)); */ break; case ItemId.EXP_COUPON_3X_2H: item.setExpiration(Server.getInstance().getCurrentTime() + HOURS.toMillis(2)); break; default: item.setExpiration(Server.getInstance().getCurrentTime() + DAYS.toMillis(1)); break; } } else { item.setExpiration(Server.getInstance().getCurrentTime() + DAYS.toMillis(period)); } } item.setSN(sn); return item; } } public static class SpecialCashItem { private final int sn; private final int modifier; private final byte info; //? public SpecialCashItem(int sn, int modifier, byte info) { this.sn = sn; this.modifier = modifier; this.info = info; } public int getSN() { return sn; } public int getModifier() { return modifier; } public byte getInfo() { return info; } } public static class CashItemFactory { private static volatile Map items = new HashMap<>(); private static volatile Map> packages = new HashMap<>(); private static volatile List specialcashitems = new ArrayList<>(); public static void loadAllCashItems() { DataProvider etc = DataProviderFactory.getDataProvider(WZFiles.ETC); Map loadedItems = new HashMap<>(); for (Data item : etc.getData("Commodity.img").getChildren()) { int sn = DataTool.getIntConvert("SN", item); int itemId = DataTool.getIntConvert("ItemId", item); int price = DataTool.getIntConvert("Price", item, 0); long period = DataTool.getIntConvert("Period", item, 1); short count = (short) DataTool.getIntConvert("Count", item, 1); boolean onSale = DataTool.getIntConvert("OnSale", item, 0) == 1; loadedItems.put(sn, new CashItem(sn, itemId, price, period, count, onSale)); } CashItemFactory.items = loadedItems; Map> loadedPackages = new HashMap<>(); for (Data cashPackage : etc.getData("CashPackage.img").getChildren()) { List cPackage = new ArrayList<>(); for (Data item : cashPackage.getChildByPath("SN").getChildren()) { cPackage.add(Integer.parseInt(item.getData().toString())); } loadedPackages.put(Integer.parseInt(cashPackage.getName()), cPackage); } CashItemFactory.packages = loadedPackages; List loadedSpecialItems = new ArrayList<>(); try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("SELECT * FROM specialcashitems"); ResultSet rs = ps.executeQuery()) { while (rs.next()) { loadedSpecialItems.add(new SpecialCashItem(rs.getInt("sn"), rs.getInt("modifier"), rs.getByte("info"))); } } catch (SQLException ex) { ex.printStackTrace(); } CashItemFactory.specialcashitems = loadedSpecialItems; } public static Optional getRandomCashItem() { if (items.isEmpty()) { return Optional.empty(); } List itemPool = items.values().stream() .filter(CashItem::isOnSale) .filter(cashItem -> !ItemId.isCashPackage(cashItem.itemId)) .toList(); return Optional.of(getRandomItem(itemPool)); } private static CashItem getRandomItem(List items) { return items.get(new Random().nextInt(items.size())); } public static CashItem getItem(int sn) { return items.get(sn); } public static List getPackage(int itemId) { List cashPackage = new ArrayList<>(); for (int sn : packages.get(itemId)) { cashPackage.add(getItem(sn).toItem()); } return cashPackage; } public static boolean isPackage(int itemId) { return packages.containsKey(itemId); } public static List getSpecialCashItems() { return specialcashitems; } } public record CashShopSurpriseResult(Item usedCashShopSurprise, Item reward) { } public int getCash(int type) { return switch (type) { case NX_CREDIT -> nxCredit; case MAPLE_POINT -> maplePoint; case NX_PREPAID -> nxPrepaid; default -> 0; }; } public void gainCash(int type, int cash) { switch (type) { case NX_CREDIT -> nxCredit += cash; case MAPLE_POINT -> maplePoint += cash; case NX_PREPAID -> nxPrepaid += cash; } } public void gainCash(int type, CashItem buyItem, int world) { gainCash(type, -buyItem.getPrice()); if (!YamlConfig.config.server.USE_ENFORCE_ITEM_SUGGESTION) { Server.getInstance().getWorld(world).addCashItemBought(buyItem.getSN()); } } public boolean isOpened() { return opened; } public void open(boolean b) { opened = b; } public List getInventory() { lock.lock(); try { return Collections.unmodifiableList(inventory); } finally { lock.unlock(); } } public Item findByCashId(int cashId) { boolean isRing; Equip equip = null; for (Item item : getInventory()) { if (item.getInventoryType().equals(InventoryType.EQUIP)) { equip = (Equip) item; isRing = equip.getRingId() > -1; } else { isRing = false; } if ((item.getPetId() > -1 ? item.getPetId() : isRing ? equip.getRingId() : item.getCashId()) == cashId) { return item; } } return null; } public void addToInventory(Item item) { lock.lock(); try { inventory.add(item); } finally { lock.unlock(); } } public void removeFromInventory(Item item) { lock.lock(); try { inventory.remove(item); } finally { lock.unlock(); } } public List getWishList() { return wishList; } public void clearWishList() { wishList.clear(); } public void addToWishList(int sn) { wishList.add(sn); } public void gift(int recipient, String from, String message, int sn) { gift(recipient, from, message, sn, -1); } public void gift(int recipient, String from, String message, int sn, int ringid) { try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("INSERT INTO `gifts` VALUES (DEFAULT, ?, ?, ?, ?, ?)")) { ps.setInt(1, recipient); ps.setString(2, from); ps.setString(3, message); ps.setInt(4, sn); ps.setInt(5, ringid); ps.executeUpdate(); } catch (SQLException sqle) { sqle.printStackTrace(); } } public List> loadGifts() { List> gifts = new ArrayList<>(); try (Connection con = DatabaseConnection.getConnection()) { try (PreparedStatement ps = con.prepareStatement("SELECT * FROM `gifts` WHERE `to` = ?")) { ps.setInt(1, characterId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { notes++; CashItem cItem = CashItemFactory.getItem(rs.getInt("sn")); Item item = cItem.toItem(); Equip equip = null; item.setGiftFrom(rs.getString("from")); if (item.getInventoryType().equals(InventoryType.EQUIP)) { equip = (Equip) item; equip.setRingId(rs.getInt("ringid")); gifts.add(new Pair<>(equip, rs.getString("message"))); } else { gifts.add(new Pair<>(item, rs.getString("message"))); } if (CashItemFactory.isPackage(cItem.getItemId())) { //Packages never contains a ring for (Item packageItem : CashItemFactory.getPackage(cItem.getItemId())) { packageItem.setGiftFrom(rs.getString("from")); addToInventory(packageItem); } } else { addToInventory(equip == null ? item : equip); } } } } try (PreparedStatement ps = con.prepareStatement("DELETE FROM `gifts` WHERE `to` = ?")) { ps.setInt(1, characterId); ps.executeUpdate(); } } catch (SQLException sqle) { sqle.printStackTrace(); } return gifts; } public int getAvailableNotes() { return notes; } public void decreaseNotes() { notes--; } public void save(Connection con) throws SQLException { try (PreparedStatement ps = con.prepareStatement("UPDATE `accounts` SET `nxCredit` = ?, `maplePoint` = ?, `nxPrepaid` = ? WHERE `id` = ?")) { ps.setInt(1, nxCredit); ps.setInt(2, maplePoint); ps.setInt(3, nxPrepaid); ps.setInt(4, accountId); ps.executeUpdate(); } List> itemsWithType = new ArrayList<>(); List inv = getInventory(); for (Item item : inv) { itemsWithType.add(new Pair<>(item, item.getInventoryType())); } factory.saveItems(itemsWithType, accountId, con); try (PreparedStatement ps = con.prepareStatement("DELETE FROM `wishlists` WHERE `charid` = ?")) { ps.setInt(1, characterId); ps.executeUpdate(); } try (PreparedStatement ps = con.prepareStatement("INSERT INTO `wishlists` VALUES (DEFAULT, ?, ?)")) { ps.setInt(1, characterId); for (int sn : wishList) { // TODO: batch insert ps.setInt(2, sn); ps.executeUpdate(); } } } public Optional openCashShopSurprise(long cashId) { lock.lock(); try { Optional maybeCashShopSurprise = getItemByCashId(cashId); if (maybeCashShopSurprise.isEmpty() || maybeCashShopSurprise.get().getItemId() != ItemId.CASH_SHOP_SURPRISE) { return Optional.empty(); } Item cashShopSurprise = maybeCashShopSurprise.get(); if (cashShopSurprise.getQuantity() <= 0) { return Optional.empty(); } if (getItemsSize() >= 100) { return Optional.empty(); } Optional cashItemReward = CashItemFactory.getRandomCashItem(); if (cashItemReward.isEmpty()) { return Optional.empty(); } short newQuantity = (short) (cashShopSurprise.getQuantity() - 1); cashShopSurprise.setQuantity(newQuantity); if (newQuantity <= 0) { removeFromInventory(cashShopSurprise); } Item itemReward = cashItemReward.get().toItem(); addToInventory(itemReward); return Optional.of(new CashShopSurpriseResult(cashShopSurprise, itemReward)); } finally { lock.unlock(); } } @GuardedBy("lock") private Optional getItemByCashId(long cashId) { return inventory.stream() .filter(item -> item.getCashId() == cashId) .findAny(); } public int getItemsSize() { lock.lock(); try { return inventory.size(); } finally { lock.unlock(); } } public static Item generateCouponItem(int itemId, short quantity) { CashItem it = new CashItem(77777777, itemId, 7777, ItemConstants.isPet(itemId) ? 30 : 0, quantity, true); return it.toItem(); } }