diff --git a/config.yaml b/config.yaml index c34d9848f0..0f350fd551 100644 --- a/config.yaml +++ b/config.yaml @@ -234,6 +234,7 @@ server: USE_STARTING_AP_4: false #Use early-GMS 4/4/4/4 starting stats. To overcome AP shortage, this gives 4AP/5AP at 1st/2nd job advancements. USE_AUTOBAN: false #Commands the server to detect infractors automatically. USE_AUTOBAN_LOG: true #Log autoban related messages. Still logs even with USE_AUTOBAN disabled. + USE_EXP_GAIN_LOG: false #Logs characters exp gains; logs world rate & coupon exp, total gained exp, and current exp, level can be calculated from "ExpTable". USE_AUTOSAVE: true #Enables server autosaving feature (saves characters to DB each 1 hour). USE_SERVER_AUTOASSIGNER: false #HeavenMS-builtin autoassigner, uses algorithm based on distributing AP accordingly with required secondary stat on equipments. USE_REFRESH_RANK_MOVE: true diff --git a/database/sql/1-db_database.sql b/database/sql/1-db_database.sql index c24fc76912..639ca90b80 100644 --- a/database/sql/1-db_database.sql +++ b/database/sql/1-db_database.sql @@ -21484,6 +21484,17 @@ CREATE TABLE IF NOT EXISTS `worldtransfers` ( INDEX (characterid) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; +CREATE TABLE IF NOT EXISTS `characterexplogs` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `world_exp_rate` INT, + `exp_coupon` INT, + `gained_exp` BIGINT, + `current_exp` INT, + `exp_gain_time` DATETIME, + `charid` int(11) NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`charid`) REFERENCES `characters`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; ALTER TABLE `dueyitems` ADD CONSTRAINT `dueyitems_ibfk_1` FOREIGN KEY (`PackageId`) REFERENCES `dueypackages` (`PackageId`) ON DELETE CASCADE; diff --git a/src/main/java/client/Character.java b/src/main/java/client/Character.java index 289abd8396..e194af2d55 100644 --- a/src/main/java/client/Character.java +++ b/src/main/java/client/Character.java @@ -59,6 +59,7 @@ import scripting.AbstractPlayerInteraction; import scripting.event.EventInstanceManager; import scripting.item.ItemScriptManager; import server.*; +import server.ExpLogger.ExpLogRecord; import server.ItemInformationProvider.ScriptedItem; import server.events.Events; import server.events.RescueGaga; @@ -150,6 +151,7 @@ public class Character extends AbstractCharacterObject { private final AtomicInteger gachaexp = new AtomicInteger(); private final AtomicInteger meso = new AtomicInteger(); private final AtomicInteger chair = new AtomicInteger(-1); + private long totalExpGained = 0; private int merchantmeso; private BuddyList buddylist; private EventInstanceManager eventInstance = null; @@ -3092,6 +3094,7 @@ public class Character extends AbstractCharacterObject { leftover = nextExp - Integer.MAX_VALUE; } updateSingleStat(Stat.EXP, exp.addAndGet((int) total)); + totalExpGained += total; if (show) { announceExpGain(gain, equip, party, inChat, white); } @@ -3108,6 +3111,20 @@ public class Character extends AbstractCharacterObject { gainExpInternal(leftover, equip, party, false, inChat, white); } else { lastExpGainTime = System.currentTimeMillis(); + + if (YamlConfig.config.server.USE_EXP_GAIN_LOG) { + ExpLogRecord expLogRecord = new ExpLogger.ExpLogRecord( + getWorldServer().getExpRate(), + expCoupon, + totalExpGained, + exp.get(), + new Timestamp(lastExpGainTime), + id + ); + ExpLogger.putExpLogRecord(expLogRecord); + } + + totalExpGained = 0; } } } diff --git a/src/main/java/config/ServerConfig.java b/src/main/java/config/ServerConfig.java index 499ac738ef..d7050da6a6 100644 --- a/src/main/java/config/ServerConfig.java +++ b/src/main/java/config/ServerConfig.java @@ -82,6 +82,7 @@ public class ServerConfig { public boolean USE_STARTING_AP_4; public boolean USE_AUTOBAN; public boolean USE_AUTOBAN_LOG; + public boolean USE_EXP_GAIN_LOG; public boolean USE_AUTOSAVE; public boolean USE_SERVER_AUTOASSIGNER; public boolean USE_REFRESH_RANK_MOVE; diff --git a/src/main/java/net/server/channel/handlers/MTSHandler.java b/src/main/java/net/server/channel/handlers/MTSHandler.java index ca00cc842f..abffbe9ad1 100644 --- a/src/main/java/net/server/channel/handlers/MTSHandler.java +++ b/src/main/java/net/server/channel/handlers/MTSHandler.java @@ -45,6 +45,8 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Calendar; import java.util.List; @@ -122,40 +124,12 @@ public final class MTSHandler extends AbstractPacketHandler { return; } } - Calendar calendar = Calendar.getInstance(); - int year; - int month; - int day; - int oldmax = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); - int oldday = calendar.get(Calendar.DAY_OF_MONTH) + 7; - if (oldmax < oldday) { - if (calendar.get(Calendar.MONTH) + 2 > 12) { - year = calendar.get(Calendar.YEAR) + 1; - month = 1; - calendar.set(year, month, 1); - day = oldday - oldmax; - } else { - month = calendar.get(Calendar.MONTH) + 2; - year = calendar.get(Calendar.YEAR); - calendar.set(year, month, 1); - day = oldday - oldmax; - } - } else { - day = calendar.get(Calendar.DAY_OF_MONTH) + 7; - month = calendar.get(Calendar.MONTH); - year = calendar.get(Calendar.YEAR); - } - String date = year + "-"; - if (month < 10) { - date += "0" + month + "-"; - } else { - date += month + "-"; - } - if (day < 10) { - date += "0" + day; - } else { - date += day + ""; - } + + LocalDate now = LocalDate.now(); + LocalDate sellEnd = now.plusDays(7); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String date = sellEnd.format(formatter); + if (!i.getInventoryType().equals(InventoryType.EQUIP)) { Item item = i; try (PreparedStatement pse = con.prepareStatement("INSERT INTO mts_items (tab, type, itemid, quantity, expiration, giftFrom, seller, price, owner, sellername, sell_ends) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { @@ -761,7 +735,7 @@ public final class MTSHandler extends AbstractPacketHandler { } } } - try (PreparedStatement ps = con.prepareStatement("SELECT COUNT(*) FROM mts_items WHERE tab = ? " + (type != 0 ? "AND type = ?" : "") + "AND transfer = 0")) { + try (PreparedStatement ps = con.prepareStatement("SELECT COUNT(*) FROM mts_items WHERE tab = ? " + (type != 0 ? "AND type = ?" : "") + " AND transfer = 0")) { ps.setInt(1, tab); if (type != 0) { ps.setInt(2, type); diff --git a/src/main/java/server/ExpLogger.java b/src/main/java/server/ExpLogger.java new file mode 100644 index 0000000000..c9016bdf69 --- /dev/null +++ b/src/main/java/server/ExpLogger.java @@ -0,0 +1,93 @@ +package server; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.sql.Timestamp; +import static java.util.concurrent.TimeUnit.*; + +import config.YamlConfig; +import tools.DatabaseConnection; + +public class ExpLogger { + private static final LinkedBlockingQueue expLoggerQueue = new LinkedBlockingQueue<>(); + private static final short EXP_LOGGER_THREAD_SLEEP_DURATION_SECONDS = 60; + private static final short EXP_LOGGER_THREAD_SHUTDOWN_WAIT_DURATION_MINUTES = 5; + + public record ExpLogRecord(int worldExpRate, int expCoupon, long gainedExp, int currentExp,Timestamp expGainTime, int charid) {} + + public static void putExpLogRecord(ExpLogRecord expLogRecord) { + try { + expLoggerQueue.put(expLogRecord); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + static private ScheduledExecutorService schdExctr = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + + private static Runnable saveExpLoggerToDBRunnable = new Runnable() { + @Override + public void run() { + try (Connection con = DatabaseConnection.getConnection(); + PreparedStatement ps = con.prepareStatement("INSERT INTO characterexplogs (world_exp_rate, exp_coupon, gained_exp, current_exp, exp_gain_time, charid) VALUES (?, ?, ?, ?, ?, ?)")) { + + List drainedExpLogs = new ArrayList<>(); + expLoggerQueue.drainTo(drainedExpLogs); + for (ExpLogRecord expLogRecord : drainedExpLogs) { + ps.setInt(1, expLogRecord.worldExpRate); + ps.setInt(2, expLogRecord.expCoupon); + ps.setLong(3, expLogRecord.gainedExp); + ps.setInt(4, expLogRecord.currentExp); + ps.setTimestamp(5, expLogRecord.expGainTime); + ps.setInt(6, expLogRecord.charid); + ps.addBatch(); + } + ps.executeBatch(); + } catch (SQLException sqle) { + sqle.printStackTrace(); + } + } + }; + + + private static void startExpLogger() { + schdExctr.scheduleWithFixedDelay(saveExpLoggerToDBRunnable, EXP_LOGGER_THREAD_SLEEP_DURATION_SECONDS, EXP_LOGGER_THREAD_SLEEP_DURATION_SECONDS, SECONDS); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + stopExpLogger(); + })); + } + + private static boolean stopExpLogger() { + schdExctr.shutdown(); + try { + schdExctr.awaitTermination(EXP_LOGGER_THREAD_SHUTDOWN_WAIT_DURATION_MINUTES, MINUTES); + Thread runThreadBeforeShutdown = new Thread(saveExpLoggerToDBRunnable); + runThreadBeforeShutdown.setPriority(Thread.MIN_PRIORITY); + runThreadBeforeShutdown.start(); + return true; + } catch (InterruptedException e) { + e.printStackTrace(); + return false; + } + } + + static { + if (YamlConfig.config.server.USE_EXP_GAIN_LOG) { + startExpLogger(); + } + } +} diff --git a/src/main/java/server/MTSItemInfo.java b/src/main/java/server/MTSItemInfo.java index 3779d69925..c091861167 100644 --- a/src/main/java/server/MTSItemInfo.java +++ b/src/main/java/server/MTSItemInfo.java @@ -23,6 +23,8 @@ package server; import client.inventory.Item; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.Calendar; /** @@ -38,13 +40,16 @@ public class MTSItemInfo { private int day = 1; public MTSItemInfo(Item item, int price, int id, int cid, String seller, String date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate sellEnd = LocalDate.parse(date, formatter); + this.item = item; this.price = price; this.seller = seller; this.id = id; - this.year = Integer.parseInt(date.substring(0, 4)); - this.month = Integer.parseInt(date.substring(5, 7)); - this.day = Integer.parseInt(date.substring(8, 10)); + this.year = sellEnd.getYear(); + this.month = sellEnd.getMonthValue(); + this.day = sellEnd.getDayOfMonth(); } public Item getItem() {