Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import net.tfminecraft.rpcharacters.objects.PlayerData;
import net.tfminecraft.rpcharacters.objects.RPCharacter;
import net.tfminecraft.rpcharacters.persona.PermissionGroupService;
import net.tfminecraft.rpcharacters.prison.PrisonSwitch;
import net.tfminecraft.rpcharacters.progress.CharacterProgressService;
import net.tfminecraft.rpcharacters.professions.ProfessionIntegrator;
import net.tfminecraft.rpcharacters.pvp.ArmourDonning;
Expand Down Expand Up @@ -474,15 +475,13 @@ public String blockReason(PlayerData account, RPCharacter character) {
BodyTag currentTag;
try { currentTag = BodyTag.read(player); }
catch (IOException error) { return "Your saved body needs recovery. Please contact staff."; }
// Prison-marked characters switch freely; the cooldown counts from the last unmarked character.
if (currentTag instanceof BodyTag.Room tag && tag.lastCharacterId() != null
&& !tag.lastCharacterId().toString().equals(character.getId())) {
RPCharacter previous = account.getCharacterById(tag.lastCharacterId().toString());
if (previous != null && previous.getStatus() == Status.ALIVE
&& !player.hasPermission("rpcharacters.no_cooldown")
&& PermissionGroupService.hasCharacterSwitchCooldown(player, account)) {
long minutes = PermissionGroupService.getRemainingCooldownMinutes(player, account);
return "You can switch in " + (minutes / 1440) + "d " + (minutes / 60 % 24) + "h " + (minutes % 60) + "m. You can resume your current character.";
}
&& PrisonSwitch.cooldownApplies(account, tag.lastCharacterId().toString(), character)
&& !player.hasPermission("rpcharacters.no_cooldown")
&& PermissionGroupService.hasCharacterSwitchCooldown(player, account)) {
long minutes = PermissionGroupService.getRemainingCooldownMinutes(player, account);
return "You can switch in " + (minutes / 1440) + "d " + (minutes / 60 % 24) + "h " + (minutes % 60) + "m. You can resume your current character.";
}
return null;
}
Expand Down Expand Up @@ -549,9 +548,7 @@ private void activate(PlayerData account, RPCharacter character, BodyRecord body
MmoInventorySlots.refresh(player);
progress.restoreResources(account, character, resources);
body.body().applyHealth(player);
if (previous.lastCharacterId() != null && !previous.lastCharacterId().equals(body.characterId())) {
account.recordCharacterSwitch();
}
PrisonSwitch.entered(account, previous.lastCharacterId() == null ? null : previous.lastCharacterId().toString(), character);
BodyTag.live(player, body.characterId(), body.rev());
live.put(owner, new BodyTag.Live(body.characterId(), body.rev()));
selecting.remove(owner);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ static Component characterCard(RPCharacter character, String blockReason) {
.append(Component.text(name(character), PALE, TextDecoration.BOLD));
line(card, joined(race(character), className(character), age(character)), TEAL);
line(card, joined(guild(character), lastPlayed(character)), MUTED);
if (character.isPrisonSwitch()) line(card, "In prison: no switch cooldown", TEAL);
if (blockReason != null) line(card, blockReason, WARN);
return card.build();
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/net/tfminecraft/rpcharacters/database/Database.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ public PlayerData loadPlayer(Player p) {
if (json.containsKey("account-attribute-points-total")) {
pd.setAccountAttributePointsTotal(((Number) json.get("account-attribute-points-total")).intValue());
}
if (json.get("last-free-character") instanceof String lastFree) {
pd.setLastFreeCharacterId(lastFree);
}
loadAccountProfessionPoints(pd, json);
loadInvestigationPoints(pd, json);
PlayerTutorialFields.load(pd, json);
Expand Down Expand Up @@ -224,6 +227,9 @@ public PlayerData loadSavedAccount(SavedAccount saved) {
if (json.containsKey("account-attribute-points-total")) {
pd.setAccountAttributePointsTotal(((Number) json.get("account-attribute-points-total")).intValue());
}
if (json.get("last-free-character") instanceof String lastFree) {
pd.setLastFreeCharacterId(lastFree);
}
loadAccountProfessionPoints(pd, json);
loadInvestigationPoints(pd, json);
PlayerTutorialFields.load(pd, json);
Expand Down Expand Up @@ -448,6 +454,9 @@ public boolean trySavePlayer(PlayerData pd) {
if (pd.getLastCharacterSwitchAtMs() != null) {
defaults.put("last-character-switch-ms", pd.getLastCharacterSwitchAtMs());
}
if (pd.getLastFreeCharacterId() != null) {
defaults.put("last-free-character", pd.getLastFreeCharacterId());
}
int i = 0;
JSONArray stageArray = new JSONArray();
while(i < pd.getCompletedStages().size()) {
Expand Down Expand Up @@ -849,6 +858,9 @@ private void loadPersonaFields(RPCharacter character, JSONObject characterJson)
if (characterJson.containsKey("hidden")) {
character.setHidden(Boolean.parseBoolean(characterJson.get("hidden").toString()));
}
if (characterJson.containsKey("prison-switch")) {
character.setPrisonSwitch(Boolean.parseBoolean(characterJson.get("prison-switch").toString()));
}
if (characterJson.containsKey("kit-statuses")) {
Object raw = characterJson.get("kit-statuses");
if (raw instanceof JSONObject statuses) {
Expand Down Expand Up @@ -951,6 +963,9 @@ private void savePersonaFields(HashMap<String, Object> defaults, RPCharacter cha
if (character.isHidden()) {
defaults.put("hidden", "true");
}
if (character.isPrisonSwitch()) {
defaults.put("prison-switch", "true");
}
if (!character.getKitStatuses().isEmpty()) {
JSONObject statuses = new JSONObject();
for (var entry : character.getKitStatuses().entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import net.tfminecraft.rpcharacters.identity.TempAliasService;
import net.tfminecraft.rpcharacters.persona.CharacterSlotService;
import net.tfminecraft.rpcharacters.evilrp.EvilRpCommands;
import net.tfminecraft.rpcharacters.prison.PrisonSwitch;
import net.tfminecraft.rpcharacters.pvp.StrikeChoice;
import net.tfminecraft.rpcharacters.party.PartyCommand;
import net.tfminecraft.rpcharacters.tutorial.TutorialCommands;
Expand Down Expand Up @@ -82,6 +83,10 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
&& args[1].equalsIgnoreCase(ClassPickReset.SUBCOMMAND)) {
return ClassPickReset.handle(sender, args, 2);
}
if (args.length >= 2 && args[0].equalsIgnoreCase("admin")
&& args[1].equalsIgnoreCase(PrisonSwitch.SUBCOMMAND)) {
return PrisonSwitch.handle(sender, args, 2);
}
if (args.length >= 1 && args[0].equalsIgnoreCase("admin")) {
return handleAdmin(sender, args);
}
Expand Down Expand Up @@ -724,7 +729,7 @@ private static boolean handleAdmin(CommandSender sender, String[] args) {
return true;
}
if (args.length < 2) {
RPTexts.send(sender, RPTexts.ERROR + "Usage: /rpcharacter admin <injure|permakill|strikes|tutorial|resetclasses|carryexp> ...");
RPTexts.send(sender, RPTexts.ERROR + "Usage: /rpcharacter admin <injure|permakill|strikes|tutorial|resetclasses|carryexp|prison> ...");
return true;
}
if (args[1].equalsIgnoreCase("injure")) {
Expand All @@ -743,7 +748,7 @@ private static boolean handleAdmin(CommandSender sender, String[] args) {
if (args[1].equalsIgnoreCase(net.tfminecraft.rpcharacters.mmocore.CarriedExperience.SUBCOMMAND)) {
return net.tfminecraft.rpcharacters.mmocore.CarriedExperience.handle(sender, args, 2);
}
RPTexts.send(sender, RPTexts.ERROR + "Usage: /rpcharacter admin <injure|permakill|strikes|tutorial|resetclasses|carryexp> ...");
RPTexts.send(sender, RPTexts.ERROR + "Usage: /rpcharacter admin <injure|permakill|strikes|tutorial|resetclasses|carryexp|prison> ...");
return true;
}

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/tfminecraft/rpcharacters/objects/PlayerData.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class PlayerData {
private Player p;
private final UUID uniqueId;
private Long lastCharacterSwitchAtMs;
/** Last character entered that was not marked for prison; the switch cooldown counts from it. */
private String lastFreeCharacterId;
private boolean eighteen;

private List<RPCharacter> characters = new ArrayList<>();
Expand Down Expand Up @@ -231,6 +233,14 @@ public void clearCharacterSwitchCooldown() {
lastCharacterSwitchAtMs = null;
}

public String getLastFreeCharacterId() {
return lastFreeCharacterId;
}

public void setLastFreeCharacterId(String lastFreeCharacterId) {
this.lastFreeCharacterId = lastFreeCharacterId;
}

public Map<String, Long> getLastKitClaimAtMsMap() {
return lastKitClaimAtMs;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public class RPCharacter {
private String alias;
private String slug;
private boolean hidden;
/** Staff mark for prison roleplay: switching into or out of this character skips the switch cooldown. */
private boolean prisonSwitch;
private boolean mailListed = true;
/** Per grant-kit id. Empty/missing kit = legacy never claim for that kit. */
private final Map<String, net.tfminecraft.rpcharacters.kit.KitStatus> kitStatuses =
Expand Down Expand Up @@ -689,6 +691,14 @@ public void setHidden(boolean hidden) {
this.hidden = hidden;
}

public boolean isPrisonSwitch() {
return prisonSwitch;
}

public void setPrisonSwitch(boolean prisonSwitch) {
this.prisonSwitch = prisonSwitch;
}

public Map<String, net.tfminecraft.rpcharacters.kit.KitStatus> getKitStatuses() {
return kitStatuses;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ private static ResolvedTarget resolveTarget(CommandSender sender, String[] args,
return new ResolvedTarget(player, character);
}

private static RPCharacter findCharacter(PlayerData pd, String query) {
/** By slug, then id, then name (ignoring case). */
public static RPCharacter findCharacter(PlayerData pd, String query) {
RPCharacter bySlug = pd.getCharacterBySlug(query);
if (bySlug != null) {
return bySlug;
Expand Down
149 changes: 149 additions & 0 deletions src/main/java/net/tfminecraft/rpcharacters/prison/PrisonSwitch.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package net.tfminecraft.rpcharacters.prison;

import java.util.logging.Logger;

import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

import net.tfminecraft.rpcharacters.Permissions;
import net.tfminecraft.rpcharacters.RPCharacters;
import net.tfminecraft.rpcharacters.enums.Status;
import net.tfminecraft.rpcharacters.managers.PlayerManager;
import net.tfminecraft.rpcharacters.objects.PlayerData;
import net.tfminecraft.rpcharacters.objects.RPCharacter;
import net.tfminecraft.rpcharacters.permadeath.PermadeathAdminCommands;
import net.tfminecraft.rpcharacters.utils.RPTexts;

/**
* Prison roleplay: staff mark an imprisoned character with {@code /rpcharacter admin prison}, and its player
* can switch into it and back out again without waiting for the character switch cooldown. The cooldown
* still holds between the account's other characters. It counts from the last unmarked character the
* player entered, so a marked character cannot carry a player from one free character to another.
*/
public final class PrisonSwitch {

public static final String SUBCOMMAND = "prison";
public static final String PERMISSION = "rpchar.prison";
static final String USAGE = "/rpcharacter admin " + SUBCOMMAND + " <player> [character] [on|off]";

private static final Logger LOG = Logger.getLogger("RPCharacters");

private PrisonSwitch() {}

/** Id of the character the switch cooldown counts from, or null when no cooldown can apply. */
static String cooldownAnchor(PlayerData account, String lastId) {
String anchor = account.getLastFreeCharacterId() != null ? account.getLastFreeCharacterId() : lastId;
if (anchor == null) {
return null;
}
RPCharacter from = account.getCharacterById(anchor);
return from != null && from.isPrisonSwitch() ? null : anchor;
}

/**
* Whether a running switch cooldown holds a move into {@code target} from {@code lastId}, the character
* the player last played: only a move away from a living unmarked character into another unmarked one.
*/
public static boolean cooldownApplies(PlayerData account, String lastId, RPCharacter target) {
if (target.isPrisonSwitch() || target.getId().equalsIgnoreCase(lastId)) {
return false;
}
String anchor = cooldownAnchor(account, lastId);
if (anchor == null || anchor.equalsIgnoreCase(target.getId())) {
return false;
}
RPCharacter from = account.getCharacterById(anchor);
return from != null && from.getStatus() == Status.ALIVE;
}

/** Records an entered character: only a move between unmarked characters starts the cooldown. */
public static void entered(PlayerData account, String lastId, RPCharacter target) {
String anchor = cooldownAnchor(account, lastId);
if (target.isPrisonSwitch() || target.getId().equalsIgnoreCase(lastId)) {
if (account.getLastFreeCharacterId() == null) {
account.setLastFreeCharacterId(anchor);
}
return;
}
if (lastId != null && anchor != null && !anchor.equalsIgnoreCase(target.getId())) {
account.recordCharacterSwitch();
}
account.setLastFreeCharacterId(target.getId());
}

public static boolean canUse(CommandSender sender) {
return sender.hasPermission(PERMISSION) || Permissions.isAdmin(sender);
}

/** {@code /rpcharacter admin prison <player> [character] [on|off]}; without on/off the mark flips. */
public static boolean handle(CommandSender sender, String[] args, int index) {
if (!canUse(sender)) {
RPTexts.send(sender, RPTexts.ERROR + "You do not have permission to use this command.");
return true;
}
int end = args.length;
Boolean wanted = null;
if (end > index + 1 && (args[end - 1].equalsIgnoreCase("on") || args[end - 1].equalsIgnoreCase("off"))) {
wanted = args[end - 1].equalsIgnoreCase("on");
end--;
}
if (end <= index || end > index + 2) {
RPTexts.send(sender, RPTexts.ERROR + "Usage: " + USAGE);
return true;
}
Player player = Bukkit.getPlayerExact(args[index]);
PlayerData account = player == null ? null : PlayerManager.get(player);
if (account == null) {
RPTexts.send(sender, RPTexts.ERROR + "No online player with loaded characters named " + RPTexts.WARN
+ args[index] + RPTexts.ERROR + ".");
return true;
}
RPCharacter character = end == index + 2
? PermadeathAdminCommands.findCharacter(account, args[index + 1])
: account.getActiveCharacter();
if (character == null || character.getStatus() != Status.ALIVE) {
RPTexts.send(sender, RPTexts.ERROR + (end == index + 2
? "No living character matching " + RPTexts.WARN + args[index + 1] + RPTexts.ERROR + " for "
: "No active character for ") + RPTexts.WARN + player.getName() + RPTexts.ERROR
+ ". Name the character: " + USAGE);
return true;
}
boolean before = character.isPrisonSwitch();
boolean marked = wanted != null ? wanted : !before;
character.setPrisonSwitch(marked);
if (!RPCharacters.getPlayerManager().saveCharacterNow(player.getUniqueId(), character)) {
character.setPrisonSwitch(before);
// A failed save may already have replaced the file, so write the old mark back before saying so.
boolean restored = RPCharacters.getPlayerManager().saveCharacterNow(player.getUniqueId(), character);
RPTexts.send(sender, RPTexts.ERROR + "Could not save " + RPTexts.WARN + character.getName() + RPTexts.ERROR
+ (restored ? ", so prison switching was not changed."
: ". Its saved prison switching may be either value until it saves again.")
+ " See the console.");
return true;
}
LOG.info("[RPCharacters] " + sender.getName() + " turned prison switching " + (marked ? "on" : "off")
+ " for " + character.getName() + " (" + character.getId() + ") of " + player.getName() + ".");
String note;
if (marked) {
RPTexts.send(sender, RPTexts.SUCCESS + "Prison switching is on for " + RPTexts.WARN + character.getName()
+ RPTexts.SUCCESS + " (" + RPTexts.WARN + player.getName() + RPTexts.SUCCESS
+ "). They can switch into and out of this character without the switch cooldown.");
note = RPTexts.WARN + character.getName() + RPTexts.INFO + " is in prison. You can switch"
+ " to and from them at any time without the character switch cooldown.";
} else {
RPTexts.send(sender, RPTexts.SUCCESS + "Prison switching is off for " + RPTexts.WARN + character.getName()
+ RPTexts.SUCCESS + " (" + RPTexts.WARN + player.getName() + RPTexts.SUCCESS
+ "). The normal switch cooldown applies again.");
note = RPTexts.WARN + character.getName() + RPTexts.INFO + " is out of prison. The"
+ " character switch cooldown applies to them again.";
}
// Chat waits for Play in character selection, so there the note goes into the selection, which redraws.
if (RPCharacters.getCharacterBodies() != null) {
RPCharacters.getCharacterBodies().feedback(player, RPTexts.formatDisplay(note));
} else {
RPTexts.send(player, note);
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import net.tfminecraft.rpcharacters.command.CharCommand;
import net.tfminecraft.rpcharacters.persona.PermissionGroupService;
import net.tfminecraft.rpcharacters.party.PartyCommand;
import net.tfminecraft.rpcharacters.prison.PrisonSwitch;
import net.tfminecraft.rpcharacters.wardrobe.WardrobeCommand;

public class CommandTabCompleter implements TabCompleter {
Expand Down Expand Up @@ -107,6 +108,10 @@ public List<String> onTabComplete(CommandSender sender, Command cmd, String labe
&& ClassPickReset.canUse(sender)) {
return filter(List.of("confirm"), args[2]);
}
if (sub.equals("admin") && args.length >= 3 && args[1].equalsIgnoreCase(PrisonSwitch.SUBCOMMAND)
&& PrisonSwitch.canUse(sender)) {
return completePrison(args);
}
if (sub.equals("admin") && args.length >= 3 && Permissions.isAdmin(sender)
&& (args[1].equalsIgnoreCase("strikes") || args[1].equalsIgnoreCase("tutorial"))) {
return completeAdminEvilRp(args);
Expand Down Expand Up @@ -173,6 +178,9 @@ public List<String> onTabComplete(CommandSender sender, Command cmd, String labe
if (ClassPickReset.canUse(sender)) {
completions.add(ClassPickReset.SUBCOMMAND);
}
if (PrisonSwitch.canUse(sender)) {
completions.add(PrisonSwitch.SUBCOMMAND);
}
} else if (args[0].equalsIgnoreCase("addtrait") || args[0].equalsIgnoreCase("removetrait")
|| args[0].equalsIgnoreCase("injure") || sub.equals("permakill")) {
for (Player online : Bukkit.getOnlinePlayers()) {
Expand Down Expand Up @@ -310,6 +318,20 @@ public List<String> onTabComplete(CommandSender sender, Command cmd, String labe
return Collections.emptyList();
}

/** {@code admin prison <player> [character] [on|off]}. */
private List<String> completePrison(String[] args) {
String last = args[args.length - 1];
if (args.length == 3) {
return filter(Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList()), last);
}
if (args.length == 4) {
List<String> completions = new ArrayList<>(characterIds(args[2]));
completions.addAll(List.of("on", "off"));
return filter(completions, last);
}
return args.length == 5 ? filter(List.of("on", "off"), last) : Collections.emptyList();
}

/** {@code admin strikes <action> <player> ...} and {@code admin tutorial reset <player> [tutorial]}. */
private List<String> completeAdminEvilRp(String[] args) {
String last = args[args.length - 1];
Expand Down
Loading
Loading