Event based and configurable player timeout system, visible through /f p. Also started storing the last player activity locally since the Bukkit API is broken. This will probably fix issues reported where players never get kicked.

This commit is contained in:
Olof Larsson 2014-10-06 13:08:34 +02:00
parent a57f05eb1d
commit 8d285eab34
8 changed files with 353 additions and 87 deletions

View File

@ -1,10 +1,13 @@
package com.massivecraft.factions.cmd; package com.massivecraft.factions.cmd;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map.Entry;
import com.massivecraft.factions.Perm; import com.massivecraft.factions.Perm;
import com.massivecraft.factions.cmd.arg.ARMPlayer; import com.massivecraft.factions.cmd.arg.ARMPlayer;
import com.massivecraft.factions.entity.MConf;
import com.massivecraft.factions.entity.MPlayer; import com.massivecraft.factions.entity.MPlayer;
import com.massivecraft.factions.event.EventFactionsRemovePlayerMillis;
import com.massivecraft.massivecore.Progressbar; import com.massivecraft.massivecore.Progressbar;
import com.massivecraft.massivecore.cmd.req.ReqHasPerm; import com.massivecraft.massivecore.cmd.req.ReqHasPerm;
import com.massivecraft.massivecore.util.TimeDiffUtil; 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); int progressbarWidth = (int) Math.round(mplayer.getPowerMax() / mplayer.getPowerMaxUniversal() * 100);
msg("<k>Power: <v>%s", Progressbar.HEALTHBAR_CLASSIC.withQuota(progressbarQuota).withWidth(progressbarWidth).render()); msg("<a>Power: <v>%s", Progressbar.HEALTHBAR_CLASSIC.withQuota(progressbarQuota).withWidth(progressbarWidth).render());
// INFO: Power (as digits) // INFO: Power (as digits)
msg("<k>Power: <v>%.2f / %.2f", mplayer.getPower(), mplayer.getPowerMax()); msg("<a>Power: <v>%.2f / %.2f", mplayer.getPower(), mplayer.getPowerMax());
// INFO: Power Boost // INFO: Power Boost
if (mplayer.hasPowerBoost()) if (mplayer.hasPowerBoost())
{ {
double powerBoost = mplayer.getPowerBoost(); double powerBoost = mplayer.getPowerBoost();
String powerBoostType = (powerBoost > 0 ? "bonus" : "penalty"); String powerBoostType = (powerBoost > 0 ? "bonus" : "penalty");
msg("<k>Power Boost: <v>%f <i>(a manually granted %s)", powerBoost, powerBoostType); msg("<a>Power Boost: <v>%f <i>(a manually granted %s)", powerBoost, powerBoostType);
} }
// INFO: Power per Hour // INFO: Power per Hour
@ -79,11 +82,33 @@ public class CmdFactionsPlayer extends FactionsCommand
stringTillMax = Txt.parse(" <i>(%s <i>left till max)", unitcountsTillMaxFormated); stringTillMax = Txt.parse(" <i>(%s <i>left till max)", unitcountsTillMaxFormated);
} }
msg("<k>Power per Hour: <v>%.2f%s", mplayer.getPowerPerHour(), stringTillMax); msg("<a>Power per Hour: <v>%.2f%s", mplayer.getPowerPerHour(), stringTillMax);
// INFO: Power per Death // INFO: Power per Death
msg("<k>Power per Death: <v>%.2f", mplayer.getPowerPerDeath()); msg("<a>Power per Death: <v>%.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("<i>Automatic removal after %s <i>of inactivity:", format(event.getMillis()));
for (Entry<String, Long> causeMillis : event.getCauseMillis().entrySet())
{
String cause = causeMillis.getKey();
long millis = causeMillis.getValue();
msg("<a>%s<a>: <v>%s", cause, format(millis));
}
}
// -------------------------------------------- //
// TIME FORMAT
// -------------------------------------------- //
public static String format(long millis)
{
LinkedHashMap<TimeUnit, Long> unitcounts = TimeDiffUtil.unitcounts(millis, TimeUnit.getAllBut(TimeUnit.MILLISECOND, TimeUnit.WEEK, TimeUnit.MONTH));
return TimeDiffUtil.formatedVerboose(unitcounts);
} }
} }

View File

@ -1103,7 +1103,8 @@ public class Faction extends Entity<Faction> implements EconomyParticipator
this.detach(); this.detach();
} }
else else
{ // promote new faction leader {
// promote new faction leader
if (oldLeader != null) if (oldLeader != null)
{ {
oldLeader.setRole(Rel.MEMBER); oldLeader.setRole(Rel.MEMBER);

View File

@ -20,6 +20,7 @@ import com.massivecraft.factions.integration.dynmap.DynmapStyle;
import com.massivecraft.factions.listeners.FactionsListenerChat; import com.massivecraft.factions.listeners.FactionsListenerChat;
import com.massivecraft.massivecore.store.Entity; import com.massivecraft.massivecore.store.Entity;
import com.massivecraft.massivecore.util.MUtil; import com.massivecraft.massivecore.util.MUtil;
import com.massivecraft.massivecore.util.TimeUnit;
public class MConf extends Entity<MConf> public class MConf extends Entity<MConf>
{ {
@ -79,8 +80,21 @@ public class MConf extends Entity<MConf>
// REMOVE DATA // REMOVE DATA
// -------------------------------------------- // // -------------------------------------------- //
public boolean removePlayerDataWhenBanned = true; public boolean removePlayerWhenBanned = true;
public double removePlayerDataAfterInactiveDays = 20.0;
// The Default
public long removePlayerMillisDefault = 10 * TimeUnit.MILLIS_PER_DAY;
// Player Age Bonus
public Map<Long, Long> removePlayerMillisPlayerAgeToBonus = MUtil.map(
2 * TimeUnit.MILLIS_PER_WEEK, 10 * TimeUnit.MILLIS_PER_DAY // +10 after 2 weeks
);
// Faction Age Bonus
public Map<Long, Long> 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 // SPECIAL FACTION IDS

View File

@ -14,6 +14,7 @@ import com.massivecraft.factions.Rel;
import com.massivecraft.factions.RelationParticipator; import com.massivecraft.factions.RelationParticipator;
import com.massivecraft.factions.event.EventFactionsChunkChange; import com.massivecraft.factions.event.EventFactionsChunkChange;
import com.massivecraft.factions.event.EventFactionsMembershipChange; import com.massivecraft.factions.event.EventFactionsMembershipChange;
import com.massivecraft.factions.event.EventFactionsRemovePlayerMillis;
import com.massivecraft.factions.event.EventFactionsMembershipChange.MembershipChangeReason; import com.massivecraft.factions.event.EventFactionsMembershipChange.MembershipChangeReason;
import com.massivecraft.factions.util.RelationUtil; import com.massivecraft.factions.util.RelationUtil;
import com.massivecraft.massivecore.mixin.Mixin; import com.massivecraft.massivecore.mixin.Mixin;
@ -43,6 +44,7 @@ public class MPlayer extends SenderEntity<MPlayer> implements EconomyParticipato
@Override @Override
public MPlayer load(MPlayer that) public MPlayer load(MPlayer that)
{ {
this.setLastActivityMillis(that.lastActivityMillis);
this.setFactionId(that.factionId); this.setFactionId(that.factionId);
this.setRole(that.role); this.setRole(that.role);
this.setTitle(that.title); this.setTitle(that.title);
@ -57,11 +59,13 @@ public class MPlayer extends SenderEntity<MPlayer> implements EconomyParticipato
@Override @Override
public boolean isDefault() 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; if (this.hasFaction()) return false;
// Role means nothing without a faction. // Role means nothing without a faction.
// Title 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.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; if (this.isUsingAdminMode()) return false;
return true; return true;
@ -99,6 +103,12 @@ public class MPlayer extends SenderEntity<MPlayer> implements EconomyParticipato
// In this section of the source code we place the field declarations only. // 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. // 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. // This is a foreign key.
// Each player belong to a faction. // Each player belong to a faction.
// Null means default. // Null means default.
@ -158,6 +168,46 @@ public class MPlayer extends SenderEntity<MPlayer> implements EconomyParticipato
this.setAutoClaimFaction(null); 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 // FIELD: factionId
// -------------------------------------------- // // -------------------------------------------- //
@ -625,6 +675,57 @@ public class MPlayer extends SenderEntity<MPlayer> implements EconomyParticipato
return BoardColl.get().getFactionAt(ps).getRelationTo(this) == Rel.ENEMY; 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 // ACTIONS
// -------------------------------------------- // // -------------------------------------------- //

View File

@ -1,13 +1,14 @@
package com.massivecraft.factions.entity; package com.massivecraft.factions.entity;
import java.util.Collection;
import org.bukkit.Bukkit;
import com.massivecraft.factions.Const; import com.massivecraft.factions.Const;
import com.massivecraft.factions.Factions; 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.MStore;
import com.massivecraft.massivecore.store.SenderColl; import com.massivecraft.massivecore.store.SenderColl;
import com.massivecraft.massivecore.util.IdUtil; import com.massivecraft.massivecore.util.IdUtil;
import com.massivecraft.massivecore.util.TimeUnit;
import com.massivecraft.massivecore.util.Txt; import com.massivecraft.massivecore.util.Txt;
public class MPlayerColl extends SenderColl<MPlayer> public class MPlayerColl extends SenderColl<MPlayer>
@ -41,87 +42,27 @@ public class MPlayerColl extends SenderColl<MPlayer>
} }
} }
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(); // For each of the offline players...
double toleranceMillis = MConf.get().removePlayerDataAfterInactiveDays * TimeUnit.MILLIS_PER_DAY; // 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<MPlayer> mplayersOffline = this.getAllOffline();
for (MPlayer mplayer : this.getAll()) Bukkit.getScheduler().runTaskAsynchronously(Factions.get(), new Runnable()
{ {
// This may or may not be required. @Override
// Some users have been reporting a loop issue with the same player detaching over and over again. public void run()
// 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)
{ {
Factions.get().log("Player "+mplayer.getName()+" was auto-removed due to inactivity."); for (MPlayer mplayer : mplayersOffline)
}
// if player is faction leader, sort out the faction since he's going away
if (mplayer.getRole() == Rel.LEADER)
{ {
Faction faction = mplayer.getFaction(); mplayer.considerRemovePlayerMillis(true);
if (faction != null)
{
mplayer.getFaction().promoteNewLeader();
} }
} }
});
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<Map<String, MPlayer>>(){}.getType();
Map<String, MPlayer> id2mplayer = Factions.get().gson.fromJson(DiscUtil.readCatch(oldFile), type);
// The Coll
MPlayerColl coll = this.getForUniverse(MassiveCore.DEFAULT);
// Set the data
for (Entry<String, MPlayer> 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();
}
}
*/
} }

View File

@ -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<String, Long> causeMillis = new LinkedHashMap<String, Long>();
public Map<String, Long> 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<Long, Long> 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<Long, Long> 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);
}
}

View File

@ -11,6 +11,7 @@ import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Enderman; import org.bukkit.entity.Enderman;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType; 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;
import com.massivecraft.factions.event.EventFactionsPowerChange.PowerChangeReason; import com.massivecraft.factions.event.EventFactionsPowerChange.PowerChangeReason;
import com.massivecraft.factions.util.VisualizeUtil; import com.massivecraft.factions.util.VisualizeUtil;
import com.massivecraft.massivecore.event.EventMassiveCorePlayerLeave;
import com.massivecraft.massivecore.mixin.Mixin; import com.massivecraft.massivecore.mixin.Mixin;
import com.massivecraft.massivecore.ps.PS; import com.massivecraft.massivecore.ps.PS;
import com.massivecraft.massivecore.util.MUtil; import com.massivecraft.massivecore.util.MUtil;
@ -96,6 +98,49 @@ public class FactionsListenerMain implements Listener
Bukkit.getPluginManager().registerEvents(this, Factions.get()); 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 // MOTD
// -------------------------------------------- // // -------------------------------------------- //
@ -512,7 +557,7 @@ public class FactionsListenerMain implements Listener
if (!player.isBanned()) return; if (!player.isBanned()) return;
// ... and we remove player data when banned ... // ... and we remove player data when banned ...
if (!MConf.get().removePlayerDataWhenBanned) return; if (!MConf.get().removePlayerWhenBanned) return;
// ... get rid of their stored info. // ... get rid of their stored info.
MPlayer mplayer = MPlayerColl.get().get(player, false); MPlayer mplayer = MPlayerColl.get().get(player, false);

View File

@ -33,7 +33,7 @@ public class TaskPlayerDataRemove extends ModuloRepeatTask
@Override @Override
public void invoke(long now) public void invoke(long now)
{ {
MPlayerColl.get().removePlayerDataAfterInactiveDaysRoutine(); MPlayerColl.get().considerRemovePlayerMillis();
} }
} }