Implement ban functionality

- Added ban module
- Added Ban and Unban commands, supporting banning and unbanning players through various command parameters.
- Added timestamp formatting method in Utils utility class for displaying ban expiration times
- Introduced PlayerErrorCode enum defining various error codes including ErrBan
- Added dual ban checking for both IP and user during player login
- Optimized login failure responses to provide specific error reasons and parameters
This commit is contained in:
HongchengQ
2025-12-10 16:33:41 -08:00
committed by Melledy
parent 6483e8a5a7
commit 3a6387c2bd
11 changed files with 750 additions and 9 deletions
@@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.game.activity.ActivityModule;
import emu.nebula.game.ban.BanModule;
import emu.nebula.game.gacha.GachaModule;
import emu.nebula.game.player.PlayerModule;
import emu.nebula.game.scoreboss.ScoreBossModule;
@@ -30,6 +31,7 @@ public class GameContext implements Runnable {
private final TutorialModule tutorialModule;
private final ActivityModule activityModule;
private final ScoreBossModule scoreBossModule;
private final BanModule banModule;
// Game loop
private final ScheduledExecutorService scheduler;
@@ -48,6 +50,7 @@ public class GameContext implements Runnable {
this.tutorialModule = new TutorialModule(this);
this.activityModule = new ActivityModule(this);
this.scoreBossModule = new ScoreBossModule(this);
this.banModule = new BanModule(this);
// Run game loop
this.scheduler = Executors.newScheduledThreadPool(1);
@@ -0,0 +1,65 @@
package emu.nebula.game.ban;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.PlayerErrorCode;
import emu.nebula.proto.Public.Error;
import emu.nebula.util.Utils;
import lombok.Getter;
import java.util.Date;
@Getter
@Entity(value = "bans", useDiscriminator = false)
public class BanInfo implements GameDatabaseObject {
@Id
private String id;
private int playerUid;
private long startTime;
private long endTime;
private String reason;
private String bannedBy;
private String ipAddress;
@Deprecated // Morphia only
public BanInfo() {
}
public BanInfo(int playerUid, long endTime, String reason, String bannedBy, String ipAddress) {
this.playerUid = playerUid;
this.startTime = System.currentTimeMillis();
this.endTime = endTime;
this.reason = reason;
this.bannedBy = bannedBy;
this.ipAddress = ipAddress;
// Generate ID based on either player UID or IP address
this.id = (ipAddress != null && !ipAddress.isEmpty() && playerUid == 0) ? "ip_" + ipAddress :
"player_" + playerUid;
}
public boolean isExpired() {
return endTime != 0 && endTime < System.currentTimeMillis();
}
public String getExpirationDateString() {
if (endTime == 0) {
return "Never";
}
return Utils.formatTimestamp(this.endTime);
}
@Override
public void save() {
GameDatabaseObject.super.save();
}
public Error toProto() {
return Error.newInstance()
.setCode(PlayerErrorCode.ErrBan.getValue())
.addArguments(
getExpirationDateString() + "\n" +
(this.reason != null ? "\n (" + this.reason + ")" : "\n" + this.id));
}
}
@@ -0,0 +1,242 @@
package emu.nebula.game.ban;
import emu.nebula.Nebula;
import emu.nebula.game.player.Player;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class BanModule extends GameContextModule {
private final Map<String, BanInfo> cachedIpBans = new ConcurrentHashMap<>();
private final Map<Integer, BanInfo> cachedPlayerBans = new ConcurrentHashMap<>();
public BanModule(GameContext context) {
super(context);
}
/**
* Ban a player by UID
* @param uid Player UID
* @param endTime Ban expiration time (0 = permanent)
* @param isBanIp Ban Ip
* @param reason Ban reason
* @param bannedBy Who banned the player
*/
public void banPlayer(int uid, long endTime, String reason, boolean isBanIp, String bannedBy) {
Player player = getGameContext().getPlayerModule().getPlayer(uid);
// It is only used as a supplement to find the banned IP when unblocking a player
String playerBindIp = null;
if (isBanIp) {
if (player != null && player.getSession() != null) {
String ipAddress = player.getSession().getIpAddress();
if (ipAddress != null && !ipAddress.isEmpty()) {
playerBindIp = ipAddress;
banIp(ipAddress, endTime, reason, bannedBy);
}
}
}
BanInfo banInfo = new BanInfo(uid, endTime, reason, bannedBy, playerBindIp);
cachedPlayerBans.put(uid, banInfo);
banInfo.save();
// Kick player
if (player != null && player.isLoaded()) {
player.setSession(null);
}
}
/**
* Ban an IP address
* <p>
* Please be cautious about disabling IPs
* in some regions where IPs are scarce, many people may share a public IP
* and restarting the optical cat device will reassign a new IP
*
* @param ipAddress IP address to ban
* @param endTime Ban expiration time (0 = permanent)
* @param reason Ban reason
* @param bannedBy Who banned the IP
*/
public void banIp(String ipAddress, long endTime, String reason, String bannedBy) {
BanInfo banInfo = new BanInfo(0, endTime, reason, bannedBy, ipAddress);
cachedIpBans.put(ipAddress, banInfo);
banInfo.save();
List<Player> playerList = Nebula.getGameContext().getPlayerModule().getCachedPlayers()
.values().stream().toList();
String playerIpAddress;
for (Player player : playerList) {
playerIpAddress = player.getSession().getIpAddress();
if (playerIpAddress == null) {
return;
}
// Kick player
if (playerIpAddress.equals(ipAddress)) {
player.setSession(null);
}
}
}
/**
* Unban a player
* @param uid Player UID
*/
public void unbanPlayer(int uid) {
BanInfo banInfo = cachedPlayerBans.remove(uid);
if (banInfo == null) {
banInfo = getPlayerBanInfo(uid);
}
if (banInfo == null) {
return;
}
deleteBan(banInfo);
}
/**
* Unban an IP address
* @param ipAddress IP address to unban
*/
public void unbanIp(String ipAddress) {
BanInfo banInfo = cachedIpBans.remove(ipAddress);
if (banInfo == null) {
banInfo = getIpBanInfo(ipAddress);
}
if (banInfo == null) {
return;
}
deleteBan(banInfo);
}
/**
* Check if a player is banned
* @param uid Player UID
* @return True if banned, false otherwise
*/
public boolean isPlayerBanned(int uid) {
BanInfo banInfo = cachedPlayerBans.get(uid);
if (banInfo == null) {
banInfo = loadPlayerBanFromDatabase(uid);
if (banInfo == null) {
return false;
}
cachedPlayerBans.put(uid, banInfo);
}
// Check if ban has expired
if (banInfo.isExpired()) {
unbanPlayer(uid);
return false;
}
return true;
}
/**
* Check if an IP address is banned
* @param ipAddress IP address
* @return True if banned, false otherwise
*/
public boolean isIpBanned(String ipAddress) {
BanInfo banInfo = cachedIpBans.get(ipAddress);
if (banInfo == null) {
banInfo = loadIpBanFromDatabase(ipAddress);
if (banInfo == null) {
return false;
}
cachedIpBans.put(ipAddress, banInfo);
}
// Check if ban has expired
if (banInfo.isExpired()) {
unbanIp(ipAddress);
return false;
}
return true;
}
/**
* Get ban info for a player
* @param uid Player UID
* @return BanInfo or null if not banned
*/
public BanInfo getPlayerBanInfo(int uid) {
BanInfo banInfo = cachedPlayerBans.get(uid);
if (banInfo == null) {
banInfo = loadPlayerBanFromDatabase(uid);
if (banInfo != null) {
cachedPlayerBans.put(uid, banInfo);
}
}
return banInfo;
}
/**
* Get ban info for an IP
* @param ipAddress IP address
* @return BanInfo or null if not banned
*/
public BanInfo getIpBanInfo(String ipAddress) {
BanInfo banInfo = cachedIpBans.get(ipAddress);
if (banInfo == null) {
banInfo = loadIpBanFromDatabase(ipAddress);
if (banInfo != null) {
cachedIpBans.put(ipAddress, banInfo);
}
}
return banInfo;
}
/**
* Delete ban from database
*/
private void deleteBan(BanInfo banInfo) {
try {
Nebula.getGameDatabase().delete(banInfo);
} catch (Exception e) {
Nebula.getLogger().error("Failed to delete ban info from database", e);
}
}
/**
* Load player ban from database
*/
private BanInfo loadPlayerBanFromDatabase(int uid) {
if (uid == 0)
return null;
try {
return Nebula.getGameDatabase().getObjectByField(BanInfo.class, "playerUid", uid);
} catch (Exception e) {
Nebula.getLogger().error("Failed to load player ban from database", e);
}
return null;
}
/**
* Load IP ban from database
*/
private BanInfo loadIpBanFromDatabase(String ipAddress) {
if (ipAddress == null || ipAddress.isEmpty())
return null;
try {
return Nebula.getGameDatabase().getObjectByField(BanInfo.class, "ipAddress", ipAddress);
} catch (Exception e) {
Nebula.getLogger().error("Failed to load IP ban from database", e);
}
return null;
}
}
@@ -0,0 +1,192 @@
package emu.nebula.game.player;
import lombok.Getter;
// resources\bin\ErrorCode.json
// resources\language\en_US\ErrorCode.json
@Getter
public enum PlayerErrorCode {
ErrInvalidToken(100101),
ErrInvalidTimestamp(100102),
ErrInvalidMsgId(100103),
ErrInvalidMsgBody(100104),
ErrAgentBusy(100105),
ErrServiceUnavailable(100106),
ErrRequestTimeout(100107),
ErrLoginFailed(100108),
ErrInvalidVersion(100109),
ErrLogin(100110),
ErrMask(110101),
ErrServerBusy(110102),
ErrData(110103),
ErrArgs(110104),
ErrInternal(110105),
ErrTokenExpire(110106),
ErrRelogin(110107),
ErrInvalidReq(110108),
ErrApiDisabled(110109),
ErrResUpdated(110110),
ErrOperateTooFast(110111),
ErrNickLenViolation(110201),
ErrNickIllegal(110202),
ErrNickForbidden(110203),
ErrBan(110205),
ErrSignatureForbidden(110206),
ErrSignatureLenViolation(110207),
ErrSignatureIllegal(110208),
ErrTitleNotFound(110209),
ErrSurveyNotFound(110210),
ErrSurveyNotOpenYet(110211),
ErrSurveyHasCompleted(110212),
ErrMailMiss(110401),
ErrMailRevoked(110402),
ErrMailUpdate(110403),
ErrMailPinned(110404),
ErrQuestNotComplete(110501),
ErrQuestClosed(110502),
ErrProductNotReady(110601),
ErrProductRestockLimit(110602),
ErrGoodsNotFound(110603),
ErrGoodsLockPurchase(110604),
ErrGoodsDelisted(110605),
ErrShopNotFound(110606),
ErrShopAlreadyClosed(110607),
ErrRecvReward(110701),
ErrMainlineCondPremission(110702),
ErrEnergyHoldLimit(110801),
ErrEnergyBuyLimit(110802),
ErrEnergyLack(110803),
ErrEnergyRestore(110804),
ErrGachaSpinLimit(110901),
ErrItemUseRefused(111001),
ErrRglInProgress(111101),
ErrRglCond(111102),
ErrRglBuildNotFound(111103),
ErrRglBuildLock(111104),
ErrFormationIdNeed(111105),
ErrFormationFullNeed(111106),
ErrBuildNameMinLenLimit(111107),
ErrBuildNameMaxLenLimit(111108),
ErrBuildNameViolation(111109),
ErrOutfitStrengthenMax(111201),
ErrOutfitNoData(111202),
ErrOutfitBeEquipped(111203),
ErrOutfitBeLocked(111204),
ErrOutfitPhaseMax(111205),
ErrOutfitStarMax(111206),
ErrOutfitLevelNotReached(111207),
ErrGiftPackageInvalid(111301),
ErrGiftPackageExpired(111302),
ErrGiftPackError(111303),
ErrRedeemCodeReachedLimit(111304),
ErrUserExchangeCountReachedLimit(111305),
ErrGiftPackageAlreadyExists(111306),
ErrGiftPackageInfoInvalid(111307),
ErrRedeemCodeItemListEmpty(111308),
ErrRedeemCodeCodeItemInvalid(111309),
ErrGiftPackageStartTimeError(111310),
ErrGiftPackageEndTimeError(111311),
ErrGiftPackageRedeemCodeQuantityExceedsLimit(111312),
ErrFixedRedeemCodeAlreadyExists(111313),
ErrFixedRedeemCodeCannotBeEmpty(111314),
ErrRedeemCodeQuantityCannotBeZero(111315),
ErrFixedRedeemCodeUserUsageQuantityRequired(111316),
ErrRedeemCodeInvalid(111317),
ErrRedeemCodeFailed(111318),
ErrRedeemCodeAlreadyUsed(111319),
ErrRedeemFailed(111320),
ErrInviteNotFound(111401),
ErrUserInviteCountLimit(111402),
ErrUserFriendCountLimit(111403),
ErrAlreadyExistFriend(111404),
ErrFriendEnergyReceiveMax(110681),
ErrMallPackageNotListed(111501),
ErrMallPackageDelisted(111502),
ErrMallPackageStockLimit(111503),
ErrMallPackageOrderLimit(111504),
ErrOrderCantRemove(111505),
ErrHeartStoneMax(111601),
ErrWeaponMaxAdvance(111602),
ErrWeaponCondNoMet(111603),
ErrCharMaxLevel(111604),
ErrCharCondMaxAdvance(111605),
ErrCharMaxAdvance(111606),
ErrCharSkillMaxLevel(111607),
ErrCharSkillCondNotMet(111608),
ErrPresentsNoData(111609),
ErrPresentsUpgradeMax(111610),
ErrPresentsBeEquipped(111611),
ErrPresentsBeLocked(111612),
ErrNotAllowedRecruit(111613),
ErrAffinityMaxLevel(111614),
ErrAffinityGiftSendCountLimit(111615),
ErrDatingCountLimit(111616),
ErrDatingSendCountLimit(111617),
ErrCharWorldClassCondNotMet(111618),
ErrDatingCharLimit(111619),
ErrCharArchiveRewardReceived(111620),
ErrReceiveEnergyFriendNotData(111621),
ErrBattlePassVersionMismatch(111701),
ErrTravelerDuelSeasonEnd(111801),
ErrProductionFormulaUnactivated(111901),
ErrTalentAlreayActiveated(112001),
ErrTalentGroupNotYetOpen(112002),
ErrTalentKeyNodesInsufficientQuantity(112003),
ErrTalentOrdinaryNodesInsufficientQuantity(112004),
ErrTalentListEmpty(112005),
ErrTalentResetFrequently(112006),
ErrTalentActivationLimit(112007),
ErrTalentNotYetActivated(112008),
ErrEquipmentNoData(112101),
ErrEquipmentUpgradeMax(112102),
ErrEquipmentBeEquipped(112103),
ErrEquipmentBeLocked(112104),
ErrDiscNoData(112201),
ErrDiscLevelNotReached(112202),
ErrDiscStrengthenMax(112203),
ErrDiscPhaseMax(112204),
ErrDiscStarMax(112205),
ErrStarTowerRankSeasonNoData(112301),
ErrStarTowerRankSeasonNotYetOpen(112302),
ErrStarTowerRankSeasonEnd(112303),
ErrAgentNoData(112401),
ErrAgentInProcess(112402),
ErrAgentCond(112403),
ErrAgentCharOccupied(112404),
ErrAgentBuildOccupied(112405),
ErrAgentBuildScoreLow(112406),
ErrAgentCountLimit(112407),
ErrAgentDailyLimit(112408),
ErrAgentWeeklyLimit(112409),
ErrAgentNotCompleted(112410),
ErrVampireSurvivorBuildNum(112501),
ErrVampireSurvivorBuildCharRepetition(112502),
ErrTowerGrowthGroupActivated(112601),
ErrJointDrillNotExist(112701),
ErrJointDrillInProgress(112702),
ErrJointDrillNotYetOpen(112703),
ErrJointDrillAlreadyClosed(112704),
ErrScoreBossNotOpen(112801),
ErrNickNameResetLimit(112901),
ErrCapOverflow(119901),
ErrConfig(119902),
ErrCondCheck(119903),
ErrNotYetOpen(119904),
ErrAlreadyClosed(119905),
ErrCondNotMet(119906),
ErrInsufficientWorldClass(119907),
ErrNoRewardsToReceive(119908),
ErrAlreayExists(119909),
ErrExchangeNotSupported(119910),
ErrRequestTooFrequent(119911),
ErrAlreaydReceive(119912),
ErrLimit(119913),
ErrDataUpdated(119914);
final int value;
PlayerErrorCode(int value) {
this.value = value;
}
}