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..6fa96bae61 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; @@ -234,7 +236,7 @@ public class Character extends AbstractCharacterObject { private final List viptrockmaps = new ArrayList<>(); private Map events = new LinkedHashMap<>(); private PartyQuest partyQuest = null; - private final List> npcUpdateQuests = new LinkedList<>(); +private final List> npcUpdateQuests = new LinkedList<>(); private Dragon dragon = null; private Ring marriageRing; private int marriageItemid = -1; @@ -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/server/ExpLogger.java b/src/main/java/server/ExpLogger.java new file mode 100644 index 0000000000..ef5591bf77 --- /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.schedule(saveExpLoggerToDBRunnable, 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(); + } + } +} \ No newline at end of file