diff --git a/src/main/java/com/massivecraft/factions/cmd/CmdFactionsPlayer.java b/src/main/java/com/massivecraft/factions/cmd/CmdFactionsPlayer.java index c793b35b..d8778baf 100644 --- a/src/main/java/com/massivecraft/factions/cmd/CmdFactionsPlayer.java +++ b/src/main/java/com/massivecraft/factions/cmd/CmdFactionsPlayer.java @@ -1,10 +1,13 @@ package com.massivecraft.factions.cmd; import java.util.LinkedHashMap; +import java.util.Map.Entry; import com.massivecraft.factions.Perm; import com.massivecraft.factions.cmd.arg.ARMPlayer; +import com.massivecraft.factions.entity.MConf; import com.massivecraft.factions.entity.MPlayer; +import com.massivecraft.factions.event.EventFactionsRemovePlayerMillis; import com.massivecraft.massivecore.Progressbar; import com.massivecraft.massivecore.cmd.req.ReqHasPerm; import com.massivecraft.massivecore.util.TimeDiffUtil; @@ -52,17 +55,17 @@ public class CmdFactionsPlayer extends FactionsCommand } int progressbarWidth = (int) Math.round(mplayer.getPowerMax() / mplayer.getPowerMaxUniversal() * 100); - msg("Power: %s", Progressbar.HEALTHBAR_CLASSIC.withQuota(progressbarQuota).withWidth(progressbarWidth).render()); + msg("Power: %s", Progressbar.HEALTHBAR_CLASSIC.withQuota(progressbarQuota).withWidth(progressbarWidth).render()); // INFO: Power (as digits) - msg("Power: %.2f / %.2f", mplayer.getPower(), mplayer.getPowerMax()); + msg("Power: %.2f / %.2f", mplayer.getPower(), mplayer.getPowerMax()); // INFO: Power Boost if (mplayer.hasPowerBoost()) { double powerBoost = mplayer.getPowerBoost(); String powerBoostType = (powerBoost > 0 ? "bonus" : "penalty"); - msg("Power Boost: %f (a manually granted %s)", powerBoost, powerBoostType); + msg("Power Boost: %f (a manually granted %s)", powerBoost, powerBoostType); } // INFO: Power per Hour @@ -79,11 +82,33 @@ public class CmdFactionsPlayer extends FactionsCommand stringTillMax = Txt.parse(" (%s left till max)", unitcountsTillMaxFormated); } - msg("Power per Hour: %.2f%s", mplayer.getPowerPerHour(), stringTillMax); + msg("Power per Hour: %.2f%s", mplayer.getPowerPerHour(), stringTillMax); // INFO: Power per Death - msg("Power per Death: %.2f", mplayer.getPowerPerDeath()); + msg("Power per Death: %.2f", mplayer.getPowerPerDeath()); + // Display automatic kick / remove info if the system is in use + if (MConf.get().removePlayerMillisDefault <= 0) return; + + EventFactionsRemovePlayerMillis event = new EventFactionsRemovePlayerMillis(false, mplayer); + event.run(); + msg("Automatic removal after %s of inactivity:", format(event.getMillis())); + for (Entry causeMillis : event.getCauseMillis().entrySet()) + { + String cause = causeMillis.getKey(); + long millis = causeMillis.getValue(); + msg("%s: %s", cause, format(millis)); + } + } + + // -------------------------------------------- // + // TIME FORMAT + // -------------------------------------------- // + + public static String format(long millis) + { + LinkedHashMap unitcounts = TimeDiffUtil.unitcounts(millis, TimeUnit.getAllBut(TimeUnit.MILLISECOND, TimeUnit.WEEK, TimeUnit.MONTH)); + return TimeDiffUtil.formatedVerboose(unitcounts); } } \ No newline at end of file diff --git a/src/main/java/com/massivecraft/factions/entity/Faction.java b/src/main/java/com/massivecraft/factions/entity/Faction.java index 6757d428..50e5865e 100644 --- a/src/main/java/com/massivecraft/factions/entity/Faction.java +++ b/src/main/java/com/massivecraft/factions/entity/Faction.java @@ -1103,7 +1103,8 @@ public class Faction extends Entity implements EconomyParticipator this.detach(); } else - { // promote new faction leader + { + // promote new faction leader if (oldLeader != null) { oldLeader.setRole(Rel.MEMBER); diff --git a/src/main/java/com/massivecraft/factions/entity/MConf.java b/src/main/java/com/massivecraft/factions/entity/MConf.java index 6833f220..4074a882 100644 --- a/src/main/java/com/massivecraft/factions/entity/MConf.java +++ b/src/main/java/com/massivecraft/factions/entity/MConf.java @@ -20,6 +20,7 @@ import com.massivecraft.factions.integration.dynmap.DynmapStyle; import com.massivecraft.factions.listeners.FactionsListenerChat; import com.massivecraft.massivecore.store.Entity; import com.massivecraft.massivecore.util.MUtil; +import com.massivecraft.massivecore.util.TimeUnit; public class MConf extends Entity { @@ -79,8 +80,21 @@ public class MConf extends Entity // REMOVE DATA // -------------------------------------------- // - public boolean removePlayerDataWhenBanned = true; - public double removePlayerDataAfterInactiveDays = 20.0; + public boolean removePlayerWhenBanned = true; + + // The Default + public long removePlayerMillisDefault = 10 * TimeUnit.MILLIS_PER_DAY; + + // Player Age Bonus + public Map removePlayerMillisPlayerAgeToBonus = MUtil.map( + 2 * TimeUnit.MILLIS_PER_WEEK, 10 * TimeUnit.MILLIS_PER_DAY // +10 after 2 weeks + ); + + // Faction Age Bonus + public Map removePlayerMillisFactionAgeToBonus = MUtil.map( + 4 * TimeUnit.MILLIS_PER_WEEK, 10 * TimeUnit.MILLIS_PER_DAY, // +10 after 4 weeks + 2 * TimeUnit.MILLIS_PER_WEEK, 5 * TimeUnit.MILLIS_PER_DAY // +5 after 2 weeks + ); // -------------------------------------------- // // SPECIAL FACTION IDS diff --git a/src/main/java/com/massivecraft/factions/entity/MPlayer.java b/src/main/java/com/massivecraft/factions/entity/MPlayer.java index 2d7b6dc8..0939d825 100644 --- a/src/main/java/com/massivecraft/factions/entity/MPlayer.java +++ b/src/main/java/com/massivecraft/factions/entity/MPlayer.java @@ -14,6 +14,7 @@ import com.massivecraft.factions.Rel; import com.massivecraft.factions.RelationParticipator; import com.massivecraft.factions.event.EventFactionsChunkChange; import com.massivecraft.factions.event.EventFactionsMembershipChange; +import com.massivecraft.factions.event.EventFactionsRemovePlayerMillis; import com.massivecraft.factions.event.EventFactionsMembershipChange.MembershipChangeReason; import com.massivecraft.factions.util.RelationUtil; import com.massivecraft.massivecore.mixin.Mixin; @@ -43,6 +44,7 @@ public class MPlayer extends SenderEntity implements EconomyParticipato @Override public MPlayer load(MPlayer that) { + this.setLastActivityMillis(that.lastActivityMillis); this.setFactionId(that.factionId); this.setRole(that.role); this.setTitle(that.title); @@ -57,11 +59,13 @@ public class MPlayer extends SenderEntity implements EconomyParticipato @Override public boolean isDefault() { + // Last activity millis is data we use for clearing out inactive players. So it does not in itself make the player data worth keeping. if (this.hasFaction()) return false; // Role means nothing without a faction. // Title means nothing without a faction. + if (this.hasPowerBoost()) return false; if (this.getPowerRounded() != (int) Math.round(MConf.get().defaultPlayerPower)) return false; - if (this.isMapAutoUpdating()) return false; + // if (this.isMapAutoUpdating()) return false; // Just having an auto updating map is not in itself reason enough for database storage. if (this.isUsingAdminMode()) return false; return true; @@ -99,6 +103,12 @@ public class MPlayer extends SenderEntity implements EconomyParticipato // In this section of the source code we place the field declarations only. // Each field has it's own section further down since just the getter and setter logic takes up quite some place. + // The last known time of explicit player activity, such as login or logout. + // Null means "unknown" (as opposed to "never played on the server"). + // The player might have been active very recently but we could lack that data. + // The reason being this data field was added in a version upgrade and has not been around for forever. + private Long lastActivityMillis = null; + // This is a foreign key. // Each player belong to a faction. // Null means default. @@ -158,6 +168,46 @@ public class MPlayer extends SenderEntity implements EconomyParticipato this.setAutoClaimFaction(null); } + // -------------------------------------------- // + // FIELD: lastActivityMillis + // -------------------------------------------- // + + // Raw: Using only what we have stored + + public Long getLastActivityMillisRaw() + { + return this.lastActivityMillis; + } + + public void setLastActivityMillis(Long lastActivityMillis) + { + // Clean input + Long target = lastActivityMillis; + + // Detect Nochange + if (MUtil.equals(this.lastActivityMillis, target)) return; + + // Apply + this.lastActivityMillis = target; + + // Mark as changed + this.changed(); + } + + public void setLastActivityMillis() + { + this.setLastActivityMillis(System.currentTimeMillis()); + } + + // Finer: Using our raw data but also underlying system as fallback + + public Long getLastActivityMillis() + { + Long ret = this.getLastActivityMillisRaw(); + if (ret != null) return ret; + return Mixin.getLastPlayed(this); + } + // -------------------------------------------- // // FIELD: factionId // -------------------------------------------- // @@ -625,6 +675,57 @@ public class MPlayer extends SenderEntity implements EconomyParticipato return BoardColl.get().getFactionAt(ps).getRelationTo(this) == Rel.ENEMY; } + // -------------------------------------------- // + // INACTIVITY TIMEOUT + // -------------------------------------------- // + + public long getRemovePlayerMillis(boolean async) + { + EventFactionsRemovePlayerMillis event = new EventFactionsRemovePlayerMillis(async, this); + event.run(); + return event.getMillis(); + } + + public boolean considerRemovePlayerMillis(boolean async) + { + // This may or may not be required. + // Some users have been reporting a loop issue with the same player detaching over and over again. + // Maybe skipping ahead if the player is detached will solve the issue. + if (this.detached()) return false; + + // Get the last activity millis. + // Null means "unknown" in which case we does nothing. + Long lastActivityMillis = this.getLastActivityMillis(); + if (lastActivityMillis == null) return false; + + // Consider + long toleranceMillis = this.getRemovePlayerMillis(async); + if (System.currentTimeMillis() - lastActivityMillis <= toleranceMillis) return false; + + // Inform + if (MConf.get().logFactionLeave || MConf.get().logFactionKick) + { + Factions.get().log("Player " + this.getName() + " was auto-removed due to inactivity."); + } + + // Apply + + // Promote a new leader if required. + if (this.getRole() == Rel.LEADER) + { + Faction faction = this.getFaction(); + if (faction != null) + { + this.getFaction().promoteNewLeader(); + } + } + + this.leave(); + this.detach(); + + return true; + } + // -------------------------------------------- // // ACTIONS // -------------------------------------------- // diff --git a/src/main/java/com/massivecraft/factions/entity/MPlayerColl.java b/src/main/java/com/massivecraft/factions/entity/MPlayerColl.java index 968c8766..1926e3ac 100644 --- a/src/main/java/com/massivecraft/factions/entity/MPlayerColl.java +++ b/src/main/java/com/massivecraft/factions/entity/MPlayerColl.java @@ -1,13 +1,14 @@ package com.massivecraft.factions.entity; +import java.util.Collection; + +import org.bukkit.Bukkit; + import com.massivecraft.factions.Const; import com.massivecraft.factions.Factions; -import com.massivecraft.factions.Rel; -import com.massivecraft.massivecore.mixin.Mixin; import com.massivecraft.massivecore.store.MStore; import com.massivecraft.massivecore.store.SenderColl; import com.massivecraft.massivecore.util.IdUtil; -import com.massivecraft.massivecore.util.TimeUnit; import com.massivecraft.massivecore.util.Txt; public class MPlayerColl extends SenderColl @@ -41,87 +42,27 @@ public class MPlayerColl extends SenderColl } } - public void removePlayerDataAfterInactiveDaysRoutine() + public void considerRemovePlayerMillis() { - if (MConf.get().removePlayerDataAfterInactiveDays <= 0.0) return; + // If the config option is 0 or below that means the server owner want it disabled. + if (MConf.get().removePlayerMillisDefault <= 0.0) return; - long now = System.currentTimeMillis(); - double toleranceMillis = MConf.get().removePlayerDataAfterInactiveDays * TimeUnit.MILLIS_PER_DAY; + // For each of the offline players... + // NOTE: If the player is currently online it's most definitely not inactive. + // NOTE: This check catches some important special cases like the @console "player". + final Collection mplayersOffline = this.getAllOffline(); - for (MPlayer mplayer : this.getAll()) + Bukkit.getScheduler().runTaskAsynchronously(Factions.get(), new Runnable() { - // This may or may not be required. - // Some users have been reporting a loop issue with the same player detaching over and over again. - // Maybe skipping ahead if the player is detached will solve the issue. - if (mplayer.detached()) continue; - - if (mplayer.isOnline()) continue; - - Long lastPlayed = Mixin.getLastPlayed(mplayer.getId()); - if (lastPlayed == null) continue; - if (now - lastPlayed <= toleranceMillis) continue; - - if (MConf.get().logFactionLeave || MConf.get().logFactionKick) + @Override + public void run() { - Factions.get().log("Player "+mplayer.getName()+" was auto-removed due to inactivity."); - } - - // if player is faction leader, sort out the faction since he's going away - if (mplayer.getRole() == Rel.LEADER) - { - Faction faction = mplayer.getFaction(); - if (faction != null) + for (MPlayer mplayer : mplayersOffline) { - mplayer.getFaction().promoteNewLeader(); + mplayer.considerRemovePlayerMillis(true); } } - - mplayer.leave(); - mplayer.detach(); - } + }); } - /* -// This method is for the 1.8.X --> 2.0.0 migration - public void migrate() - { - // Create file objects - File oldFile = new File(Factions.get().getDataFolder(), "players.json"); - File newFile = new File(Factions.get().getDataFolder(), "players.json.migrated"); - - // Already migrated? - if ( ! oldFile.exists()) return; - - // Read the file content through GSON. - Type type = new TypeToken>(){}.getType(); - Map id2mplayer = Factions.get().gson.fromJson(DiscUtil.readCatch(oldFile), type); - - // The Coll - MPlayerColl coll = this.getForUniverse(MassiveCore.DEFAULT); - - // Set the data - for (Entry entry : id2mplayer.entrySet()) - { - String playerId = entry.getKey(); - MPlayer mplayer = entry.getValue(); - coll.attach(mplayer, playerId); - } - - // Mark as migrated - oldFile.renameTo(newFile); - } - - // -------------------------------------------- // - // EXTRAS - // -------------------------------------------- // - - public void clean() - { - for (MPlayerColl coll : this.getColls()) - { - coll.clean(); - } - } - */ - } diff --git a/src/main/java/com/massivecraft/factions/event/EventFactionsRemovePlayerMillis.java b/src/main/java/com/massivecraft/factions/event/EventFactionsRemovePlayerMillis.java new file mode 100644 index 00000000..37894970 --- /dev/null +++ b/src/main/java/com/massivecraft/factions/event/EventFactionsRemovePlayerMillis.java @@ -0,0 +1,139 @@ +package com.massivecraft.factions.event; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.bukkit.event.HandlerList; + +import com.massivecraft.factions.entity.Faction; +import com.massivecraft.factions.entity.MConf; +import com.massivecraft.factions.entity.MPlayer; +import com.massivecraft.massivecore.event.EventMassiveCore; + +public class EventFactionsRemovePlayerMillis extends EventMassiveCore +{ + // -------------------------------------------- // + // REQUIRED EVENT CODE + // -------------------------------------------- // + + private static final HandlerList handlers = new HandlerList(); + @Override public HandlerList getHandlers() { return handlers; } + public static HandlerList getHandlerList() { return handlers; } + + // -------------------------------------------- // + // FIELD + // -------------------------------------------- // + + private final MPlayer mplayer; + public MPlayer getMPlayer() { return this.mplayer; } + + private long millis; + public long getMillis() { return this.millis; } + public void setMillis(long millis) { this.millis = millis; } + + private Map causeMillis = new LinkedHashMap(); + public Map getCauseMillis() { return this.causeMillis; } + + // -------------------------------------------- // + // CONSTRUCT + // -------------------------------------------- // + + public EventFactionsRemovePlayerMillis(boolean async, MPlayer mplayer) + { + super(async); + + this.mplayer = mplayer; + this.millis = MConf.get().removePlayerMillisDefault; + + // Default + this.causeMillis.put("Default", MConf.get().removePlayerMillisDefault); + + // Player Age Bonus + this.applyPlayerAgeBonus(); + + // Faction Age Bonus + this.applyFactionAgeBonus(); + } + + // -------------------------------------------- // + // UTIL + // -------------------------------------------- // + + public void applyPlayerAgeBonus() + { + // Skip if this bonus is totally disabled. + // We don't want it showing up with 0 for everyone. + if (MConf.get().removePlayerMillisPlayerAgeToBonus.isEmpty()) return; + + // Calculate First Played + Long firstPlayed = this.getMPlayer().getFirstPlayed(); + Long age = 0L; + if (firstPlayed != null) + { + age = System.currentTimeMillis() - firstPlayed; + } + + // Calculate the Bonus! + long bonus = 0; + for (Entry entry : MConf.get().removePlayerMillisPlayerAgeToBonus.entrySet()) + { + Long key = entry.getKey(); + if (key == null) continue; + + Long value = entry.getValue(); + if (value == null) continue; + + if (age >= key) + { + bonus = value; + break; + } + } + + // Apply + this.setMillis(this.getMillis() + bonus); + + // Inform + this.getCauseMillis().put("Player Age Bonus", bonus); + } + + public void applyFactionAgeBonus() + { + // Skip if this bonus is totally disabled. + // We don't want it showing up with 0 for everyone. + if (MConf.get().removePlayerMillisFactionAgeToBonus.isEmpty()) return; + + // Calculate Faction Age + Faction faction = this.getMPlayer().getFaction(); + long age = 0; + if ( ! faction.isNone()) + { + age = System.currentTimeMillis() - faction.getCreatedAtMillis(); + } + + // Calculate the Bonus! + long bonus = 0; + for (Entry entry : MConf.get().removePlayerMillisFactionAgeToBonus.entrySet()) + { + Long key = entry.getKey(); + if (key == null) continue; + + Long value = entry.getValue(); + if (value == null) continue; + + if (age >= key) + { + bonus = value; + break; + } + } + + // Apply + this.setMillis(this.getMillis() + bonus); + + // Inform + this.getCauseMillis().put("Faction Age Bonus", bonus); + } + +} diff --git a/src/main/java/com/massivecraft/factions/listeners/FactionsListenerMain.java b/src/main/java/com/massivecraft/factions/listeners/FactionsListenerMain.java index abbe57ce..aa324500 100644 --- a/src/main/java/com/massivecraft/factions/listeners/FactionsListenerMain.java +++ b/src/main/java/com/massivecraft/factions/listeners/FactionsListenerMain.java @@ -11,6 +11,7 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Enderman; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; @@ -71,6 +72,7 @@ import com.massivecraft.factions.event.EventFactionsPvpDisallowed; import com.massivecraft.factions.event.EventFactionsPowerChange; import com.massivecraft.factions.event.EventFactionsPowerChange.PowerChangeReason; import com.massivecraft.factions.util.VisualizeUtil; +import com.massivecraft.massivecore.event.EventMassiveCorePlayerLeave; import com.massivecraft.massivecore.mixin.Mixin; import com.massivecraft.massivecore.ps.PS; import com.massivecraft.massivecore.util.MUtil; @@ -95,7 +97,50 @@ public class FactionsListenerMain implements Listener { Bukkit.getPluginManager().registerEvents(this, Factions.get()); } + + // -------------------------------------------- // + // UPDATE LAST ACTIVITY + // -------------------------------------------- // + public static void updateLastActivity(CommandSender sender) + { + if (sender == null) throw new RuntimeException("sender"); + MPlayer mplayer = MPlayer.get(sender); + mplayer.setLastActivityMillis(); + } + + public static void updateLastActivitySoon(final CommandSender sender) + { + if (sender == null) throw new RuntimeException("sender"); + Bukkit.getScheduler().scheduleSyncDelayedTask(Factions.get(), new Runnable() + { + @Override + public void run() + { + updateLastActivity(sender); + } + }); + } + + // Can't be cancelled + @EventHandler(priority = EventPriority.LOWEST) + public void updateLastActivity(PlayerJoinEvent event) + { + // During the join event itself we want to be able to reach the old data. + // That is also the way the underlying fallback Mixin system does it and we do it that way for the sake of symmetry. + // For that reason we wait till the next tick with updating the value. + updateLastActivitySoon(event.getPlayer()); + } + + // Can't be cancelled + @EventHandler(priority = EventPriority.LOWEST) + public void updateLastActivity(EventMassiveCorePlayerLeave event) + { + // Here we do however update immediately. + // The player data should be fully updated before leaving the server. + updateLastActivity(event.getPlayer()); + } + // -------------------------------------------- // // MOTD // -------------------------------------------- // @@ -512,7 +557,7 @@ public class FactionsListenerMain implements Listener if (!player.isBanned()) return; // ... and we remove player data when banned ... - if (!MConf.get().removePlayerDataWhenBanned) return; + if (!MConf.get().removePlayerWhenBanned) return; // ... get rid of their stored info. MPlayer mplayer = MPlayerColl.get().get(player, false); diff --git a/src/main/java/com/massivecraft/factions/task/TaskPlayerDataRemove.java b/src/main/java/com/massivecraft/factions/task/TaskPlayerDataRemove.java index 152cd2df..a6e6d89d 100644 --- a/src/main/java/com/massivecraft/factions/task/TaskPlayerDataRemove.java +++ b/src/main/java/com/massivecraft/factions/task/TaskPlayerDataRemove.java @@ -33,7 +33,7 @@ public class TaskPlayerDataRemove extends ModuloRepeatTask @Override public void invoke(long now) { - MPlayerColl.get().removePlayerDataAfterInactiveDaysRoutine(); + MPlayerColl.get().considerRemovePlayerMillis(); } }