Skip to content
Open
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ PvP Toggle gives players control over their PvP status. Players can choose to op
- **Per-player PvP toggle** - Each player can independently enable or disable their PvP status
- **Combat protection** - Players cannot toggle PvP while actively in combat
- **Toggle cooldown** - Configurable cooldown between PvP state changes to prevent abuse
- **Off timeout** - Delay before PvP can be turned off after enabling
- **Persistent state** - PvP status is saved across server restarts
- **Admin controls** - Server administrators can view and modify plugin settings in-game

Expand All @@ -23,6 +24,7 @@ When a player has PvP disabled:
If either the attacker or the target has PvP disabled, no damage is dealt.

The combat timer prevents players from disabling PvP mid-fight. After engaging in PvP combat, players must wait for the combat timer to expire before they can change their PvP status.
The off timeout (if configured) requires players to keep PvP enabled for a minimum time before turning it off, and will queue the request to disable when used early.

## Commands

Expand All @@ -47,6 +49,7 @@ The combat timer prevents players from disabling PvP mid-fight. After engaging i
|------------------|------|----------------------------------------------------------------|
| `combattimer` | seconds | How long after PvP damage before you can toggle (0 to disable) |
| `cooldown` | seconds | How long between PvP toggles (0 to disable) |
| `offtimeout` | seconds | Minimum time after enabling PvP before disabling (0 to disable) |
| `default` | true/false | Default PvP state for new players |
| `persist` | true/false | Save PvP state across server restarts |
| `itemprotection` | true/false | Enable item protection for PvP players |
Expand All @@ -59,6 +62,7 @@ Boolean values accept: `true`, `false`, `yes`, `no`, `on`, `off`, `1`, `0`
|-------------------|------------|
| Combat Timer | 10 seconds |
| Toggle Cooldown | 5 seconds |
| Off Timeout | 0 seconds |
| Default PvP State | Disabled |
| Data Persistence | Enabled |
| Item Protection | Enabled |
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/me/jack/pvptoggle/PvPTogglePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import me.jack.pvptoggle.config.PvPToggleConfig;
import me.jack.pvptoggle.systems.CombatTrackingSystem;
import me.jack.pvptoggle.systems.PreventDamageSystem;
import me.jack.pvptoggle.systems.PvPOffTimeoutSystem;
import me.jack.pvptoggle.systems.PvPItemProtectionSystem;

import javax.annotation.Nonnull;
Expand Down Expand Up @@ -70,6 +71,7 @@ protected void setup() {
this.getEntityStoreRegistry().registerSystem(new CombatTrackingSystem());
this.getEntityStoreRegistry().registerSystem(new PreventDamageSystem());
this.getEntityStoreRegistry().registerSystem(new PvPItemProtectionSystem());
this.getEntityStoreRegistry().registerSystem(new PvPOffTimeoutSystem());

this.getCommandRegistry().registerCommand(new PvPCommand());
}
Expand All @@ -85,4 +87,4 @@ private void onPlayerReady(@Nonnull PlayerReadyEvent event) {
playerRef.getStore().ensureComponent(playerRef, this.pvpToggleComponentType);
}
}
}
}
12 changes: 11 additions & 1 deletion src/main/java/me/jack/pvptoggle/commands/PvPOffCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import me.jack.pvptoggle.components.PvPToggleComponent;
import me.jack.pvptoggle.util.PvPToggleMessageUtil;
import org.checkerframework.checker.nullness.compatqual.NonNullDecl;

import java.time.Instant;
Expand Down Expand Up @@ -49,13 +50,22 @@ protected void execute(
return;
}

long remainingOffTimeout = pvp.getRemainingOffTimeoutSeconds();
if (remainingOffTimeout > 0) {
pvp.setPendingDisableAt(pvp.getOffTimeoutEndTime());
pvp.setLastDisableAnnouncementSeconds(remainingOffTimeout);
commandContext.sendMessage(PvPToggleMessageUtil.buildDisableCountdownMessage(remainingOffTimeout));
return;
}

if (pvp.isOnCooldown()) {
commandContext.sendMessage(Message.translation("pvptoggle.toggle_cooldown").param("timeLeft", pvp.getRemainingCooldown()));
return;
}

pvp.setPvPEnabled(false);
pvp.setLastToggleTime(Instant.now());
pvp.clearPendingDisable();
commandContext.sendMessage(Message.translation("pvptoggle.off"));
}
}
}
5 changes: 4 additions & 1 deletion src/main/java/me/jack/pvptoggle/commands/PvPOnCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import me.jack.pvptoggle.components.PvPToggleComponent;
import me.jack.pvptoggle.util.PvPToggleMessageUtil;
import org.checkerframework.checker.nullness.compatqual.NonNullDecl;

import java.time.Instant;
Expand Down Expand Up @@ -56,6 +57,8 @@ protected void execute(

pvp.setPvPEnabled(true);
pvp.setLastToggleTime(Instant.now());
pvp.clearPendingDisable();
commandContext.sendMessage(Message.translation("pvptoggle.on"));
world.sendMessage(PvPToggleMessageUtil.buildPublicPvpOnMessage(playerRef.getUsername()));
}
}
}
22 changes: 19 additions & 3 deletions src/main/java/me/jack/pvptoggle/commands/PvPToggleCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import me.jack.pvptoggle.components.PvPToggleComponent;
import me.jack.pvptoggle.util.PvPToggleMessageUtil;
import org.checkerframework.checker.nullness.compatqual.NonNullDecl;

import java.time.Instant;
Expand Down Expand Up @@ -44,15 +45,30 @@ protected void execute(
return;
}

if (pvp.isPvPEnabled()) {
long remainingOffTimeout = pvp.getRemainingOffTimeoutSeconds();
if (remainingOffTimeout > 0) {
pvp.setPendingDisableAt(pvp.getOffTimeoutEndTime());
pvp.setLastDisableAnnouncementSeconds(remainingOffTimeout);
commandContext.sendMessage(PvPToggleMessageUtil.buildDisableCountdownMessage(remainingOffTimeout));
return;
}
}

if (pvp.isOnCooldown()) {
commandContext.sendMessage(Message.translation("pvptoggle.toggle_cooldown").param("timeLeft", pvp.getRemainingCooldown()));
return;
}

String messageKey = pvp.isPvPEnabled() ? "pvptoggle.off" : "pvptoggle.on";
boolean newState = !pvp.isPvPEnabled();
String messageKey = newState ? "pvptoggle.on" : "pvptoggle.off";

pvp.setPvPEnabled(!pvp.isPvPEnabled());
pvp.setPvPEnabled(newState);
pvp.setLastToggleTime(Instant.now());
pvp.clearPendingDisable();
commandContext.sendMessage(Message.translation(messageKey));
if (newState) {
world.sendMessage(PvPToggleMessageUtil.buildPublicPvpOnMessage(playerRef.getUsername()));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ protected CompletableFuture<Void> execute(@Nonnull CommandContext context) {
: Message.translation("pvptoggle.common.disabled");
context.sendMessage(Message.translation("pvptoggle.config.toggle_cooldown").param("value", cooldownValue));

long offTimeout = config.getOffTimeoutSeconds();
Message offTimeoutValue = offTimeout > 0
? Message.translation("pvptoggle.common.seconds").param("count", offTimeout)
: Message.translation("pvptoggle.common.disabled");
context.sendMessage(Message.translation("pvptoggle.config.off_timeout").param("value", offTimeoutValue));

context.sendMessage(Message.translation("pvptoggle.config.default_state")
.param("state", Message.translation(config.isDefaultPvPEnabled() ? "pvptoggle.common.enabled" : "pvptoggle.common.disabled")));

Expand All @@ -49,11 +55,12 @@ protected CompletableFuture<Void> execute(@Nonnull CommandContext context) {
context.sendMessage(Message.translation("pvptoggle.config.help.title"));
context.sendMessage(Message.translation("pvptoggle.config.help.combat"));
context.sendMessage(Message.translation("pvptoggle.config.help.cooldown"));
context.sendMessage(Message.translation("pvptoggle.config.help.offtimeout"));
context.sendMessage(Message.translation("pvptoggle.config.help.default"));
context.sendMessage(Message.translation("pvptoggle.config.help.persist"));
context.sendMessage(Message.translation("pvptoggle.config.help.itemprotection"));
context.sendMessage(Message.translation("pvptoggle.config.help.knockback"));

return CompletableFuture.completedFuture(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class PvPAdminSetCommand extends AbstractCommand {

public PvPAdminSetCommand() {
super("set", "Set a config value");
this.keyArg = this.withRequiredArg("key", "Config key (combattimer, cooldown, default, persist, itemprotection)", ArgTypes.STRING);
this.keyArg = this.withRequiredArg("key", "Config key (combattimer, cooldown, offtimeout, default, persist, itemprotection, knockback)", ArgTypes.STRING);
this.valueArg = this.withRequiredArg("value", "New value", ArgTypes.STRING);
}

Expand Down Expand Up @@ -57,6 +57,16 @@ protected CompletableFuture<Void> execute(@Nonnull CommandContext context) {
Message timerMsg = Message.translation(seconds == 0 ? "pvptoggle.common.seconds_disabled" : "pvptoggle.common.seconds").param("count", seconds);
context.sendMessage(Message.translation("pvptoggle.admin.set.toggle_cooldown").param("value", timerMsg));
}
case "offtimeout" -> {
Long seconds = parseLong(value);
if (seconds == null) {
context.sendMessage(MSG_INVALID_VALUE);
return CompletableFuture.completedFuture(null);
}
config.setOffTimeoutSeconds(seconds);
Message timerMsg = Message.translation(seconds == 0 ? "pvptoggle.common.seconds_disabled" : "pvptoggle.common.seconds").param("count", seconds);
context.sendMessage(Message.translation("pvptoggle.admin.set.off_timeout").param("value", timerMsg));
}
case "default" -> {
Boolean enabled = parseBoolean(value);
if (enabled == null) {
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/me/jack/pvptoggle/components/PvPToggleComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class PvPToggleComponent implements Component {
private boolean pvpEnabled;
private Instant lastToggleTime = Instant.EPOCH;
private Instant lastCombatTime = Instant.EPOCH;
private Instant pendingDisableAt = Instant.EPOCH;
private long lastDisableAnnouncementSeconds = -1;

public PvPToggleComponent() {
this.pvpEnabled = PvPTogglePlugin.CONFIG.get().isDefaultPvPEnabled();
Expand Down Expand Up @@ -57,6 +59,51 @@ public void setLastCombatTime(Instant lastCombatTime) {
this.lastCombatTime = lastCombatTime;
}

public boolean hasPendingDisable() {
return !Instant.EPOCH.equals(this.pendingDisableAt);
}

public Instant getPendingDisableAt() {
return pendingDisableAt;
}

public void setPendingDisableAt(Instant pendingDisableAt) {
this.pendingDisableAt = pendingDisableAt;
}

public void clearPendingDisable() {
this.pendingDisableAt = Instant.EPOCH;
this.lastDisableAnnouncementSeconds = -1;
}

public long getRemainingPendingDisableSeconds() {
if (!hasPendingDisable()) return 0;
return Math.max(0, pendingDisableAt.getEpochSecond() - Instant.now().getEpochSecond());
}

public long getRemainingOffTimeoutSeconds() {
if (!pvpEnabled) return 0;
long timeout = PvPTogglePlugin.CONFIG.get().getOffTimeoutSeconds();
if (timeout <= 0) return 0;
Instant disableAt = this.lastToggleTime.plusSeconds(timeout);
return Math.max(0, disableAt.getEpochSecond() - Instant.now().getEpochSecond());
}

public Instant getOffTimeoutEndTime() {
if (!pvpEnabled) return Instant.EPOCH;
long timeout = PvPTogglePlugin.CONFIG.get().getOffTimeoutSeconds();
if (timeout <= 0) return Instant.EPOCH;
return this.lastToggleTime.plusSeconds(timeout);
}

public long getLastDisableAnnouncementSeconds() {
return lastDisableAnnouncementSeconds;
}

public void setLastDisableAnnouncementSeconds(long lastDisableAnnouncementSeconds) {
this.lastDisableAnnouncementSeconds = lastDisableAnnouncementSeconds;
}

public boolean isInCombat() {
long duration = PvPTogglePlugin.CONFIG.get().getCombatTimerSeconds();
if (duration <= 0) return false;
Expand Down Expand Up @@ -89,6 +136,8 @@ public Component clone() {

clone.lastCombatTime = this.lastCombatTime;
clone.lastToggleTime = this.lastToggleTime;
clone.pendingDisableAt = this.pendingDisableAt;
clone.lastDisableAnnouncementSeconds = this.lastDisableAnnouncementSeconds;

return clone;
}
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/me/jack/pvptoggle/config/PvPToggleConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class PvPToggleConfig {

private long combatTimerSeconds = PvPToggleConstants.COMBAT_TIMER_SECONDS;
private long toggleCooldownSeconds = PvPToggleConstants.TOGGLE_COOLDOWN_SECONDS;
private long offTimeoutSeconds = PvPToggleConstants.OFF_TIMEOUT_SECONDS;

public static final BuilderCodec<PvPToggleConfig> CODEC = BuilderCodec
.builder(PvPToggleConfig.class, PvPToggleConfig::new)
Expand All @@ -28,6 +29,9 @@ public class PvPToggleConfig {
.append(new KeyedCodec<>("ToggleCooldownSeconds", Codec.LONG),
(config, value) -> config.toggleCooldownSeconds = value,
(config) -> config.toggleCooldownSeconds).add()
.append(new KeyedCodec<>("OffTimeoutSeconds", Codec.LONG),
(config, value) -> config.offTimeoutSeconds = value,
(config) -> config.offTimeoutSeconds).add()
.append(new KeyedCodec<>("ItemProtectionEnabled", Codec.BOOLEAN),
(config, value) -> config.itemProtectionEnabled = value,
(config) -> config.itemProtectionEnabled).add()
Expand Down Expand Up @@ -76,6 +80,16 @@ public PvPToggleConfig setToggleCooldownSeconds(long toggleCooldownSeconds) {
return this;
}

public long getOffTimeoutSeconds() {
return offTimeoutSeconds;
}

public PvPToggleConfig setOffTimeoutSeconds(long offTimeoutSeconds) {
this.offTimeoutSeconds = offTimeoutSeconds;

return this;
}

public boolean isItemProtectionEnabled() {
return itemProtectionEnabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public class PvPToggleConstants {

public static final long TOGGLE_COOLDOWN_SECONDS = 5;
public static final long COMBAT_TIMER_SECONDS = 10;
public static final long OFF_TIMEOUT_SECONDS = 0;
}
82 changes: 82 additions & 0 deletions src/main/java/me/jack/pvptoggle/systems/PvPOffTimeoutSystem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package me.jack.pvptoggle.systems;

import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import me.jack.pvptoggle.components.PvPToggleComponent;
import me.jack.pvptoggle.util.PvPToggleMessageUtil;
import org.checkerframework.checker.nullness.compatqual.NonNullDecl;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;

import java.time.Instant;

public class PvPOffTimeoutSystem extends EntityTickingSystem<EntityStore> {
@Override
public void tick(
float dt,
int index,
@NonNullDecl ArchetypeChunk<EntityStore> archetypeChunk,
@NonNullDecl Store<EntityStore> store,
@NonNullDecl CommandBuffer<EntityStore> commandBuffer
) {
Ref<EntityStore> ref = archetypeChunk.getReferenceTo(index);
PvPToggleComponent pvp = (PvPToggleComponent) commandBuffer.getComponent(ref, PvPToggleComponent.getComponentType());

if (pvp == null) {
return;
}

if (!pvp.isPvPEnabled()) {
if (pvp.hasPendingDisable()) {
pvp.clearPendingDisable();
}
return;
}

if (!pvp.hasPendingDisable()) {
return;
}

long remainingSeconds = pvp.getRemainingPendingDisableSeconds();

if (remainingSeconds <= 0) {
if (pvp.isInCombat()) {
return;
}

pvp.setPvPEnabled(false);
pvp.setLastToggleTime(Instant.now());
pvp.clearPendingDisable();

PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
playerRef.sendMessage(Message.translation("pvptoggle.off"));
}
return;
}

if (remainingSeconds == pvp.getLastDisableAnnouncementSeconds()) {
return;
}

if (remainingSeconds <= 10 || (remainingSeconds >= 60 && remainingSeconds % 60 == 0)) {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
playerRef.sendMessage(PvPToggleMessageUtil.buildDisableCountdownMessage(remainingSeconds));
}
pvp.setLastDisableAnnouncementSeconds(remainingSeconds);
}
}

@NullableDecl
@Override
public Query<EntityStore> getQuery() {
return PlayerRef.getComponentType();
}
}
Loading