Initial commit
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Gradle
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
classes/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Fabric / Loom
|
||||||
|
run/
|
||||||
|
remappedSrc/
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
56
build.gradle
Normal file
56
build.gradle
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
plugins {
|
||||||
|
id 'net.fabricmc.fabric-loom-remap' version "${loom_version}"
|
||||||
|
id 'maven-publish'
|
||||||
|
}
|
||||||
|
|
||||||
|
version = project.mod_version
|
||||||
|
group = project.maven_group
|
||||||
|
|
||||||
|
base {
|
||||||
|
archivesName = 'respawn-backoff'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
}
|
||||||
|
|
||||||
|
loom {
|
||||||
|
splitEnvironmentSourceSets()
|
||||||
|
|
||||||
|
mods {
|
||||||
|
"respawn_backoff" {
|
||||||
|
sourceSet sourceSets.main
|
||||||
|
sourceSet sourceSets.client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||||
|
mappings loom.officialMojangMappings()
|
||||||
|
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||||
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
inputs.property "version", project.version
|
||||||
|
|
||||||
|
filesMatching("fabric.mod.json") {
|
||||||
|
expand "version": inputs.properties.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
it.options.release = 21
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
from("LICENSE") {
|
||||||
|
rename { "${it}_${base.archivesName.get()}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
gradle.properties
Normal file
12
gradle.properties
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx1G
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.configuration-cache=false
|
||||||
|
|
||||||
|
minecraft_version=1.21.1
|
||||||
|
loader_version=0.18.5
|
||||||
|
loom_version=1.15-SNAPSHOT
|
||||||
|
|
||||||
|
mod_version=1.0.0
|
||||||
|
maven_group=net.respawnbackoff
|
||||||
|
|
||||||
|
fabric_api_version=0.116.10+1.21.1
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
||||||
|
networkTimeout=120000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
111
gradlew
vendored
Executable file
111
gradlew
vendored
Executable file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"}
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in
|
||||||
|
/*) app_path=$link ;;
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in
|
||||||
|
CYGWIN*) cygwin=true ;;
|
||||||
|
Darwin*) darwin=true ;;
|
||||||
|
MSYS* | MINGW*) msys=true ;;
|
||||||
|
NONSTOP*) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case $MAX_FD in
|
||||||
|
'' | soft) ;;
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
12
settings.gradle
Normal file
12
settings.gradle
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = 'Fabric'
|
||||||
|
url = 'https://maven.fabricmc.net/'
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = 'respawn-backoff'
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package net.respawnbackoff.client;
|
||||||
|
|
||||||
|
import net.fabricmc.api.ClientModInitializer;
|
||||||
|
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||||
|
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.respawnbackoff.CooldownSyncPayload;
|
||||||
|
|
||||||
|
public class RespawnBackoffClient implements ClientModInitializer {
|
||||||
|
private static volatile boolean overlayActive;
|
||||||
|
private static volatile long cooldownEndEpochMs;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitializeClient() {
|
||||||
|
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> clearForDisconnect());
|
||||||
|
|
||||||
|
ClientPlayNetworking.registerGlobalReceiver(CooldownSyncPayload.TYPE, (payload, context) -> {
|
||||||
|
context.client().execute(() -> {
|
||||||
|
overlayActive = payload.active();
|
||||||
|
cooldownEndEpochMs = payload.cooldownEndEpochMs();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
HudRenderCallback.EVENT.register((guiGraphics, tickDelta) -> {
|
||||||
|
if (!overlayActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Minecraft client = Minecraft.getInstance();
|
||||||
|
if (client.player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long remainingMs = Math.max(0L, cooldownEndEpochMs - System.currentTimeMillis());
|
||||||
|
renderOverlay(guiGraphics, client, remainingMs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderOverlay(GuiGraphics graphics, Minecraft client, long remainingMs) {
|
||||||
|
int w = client.getWindow().getGuiScaledWidth();
|
||||||
|
int h = client.getWindow().getGuiScaledHeight();
|
||||||
|
graphics.fill(0, 0, w, h, 0xFF000000);
|
||||||
|
|
||||||
|
int totalSeconds = (int) (remainingMs / 1000L);
|
||||||
|
int minutes = totalSeconds / 60;
|
||||||
|
int seconds = totalSeconds % 60;
|
||||||
|
String time = String.format("%02d:%02d", minutes, seconds);
|
||||||
|
Component line = Component.translatable("respawn_backoff.hud.countdown", time);
|
||||||
|
|
||||||
|
int textWidth = client.font.width(line);
|
||||||
|
graphics.drawString(
|
||||||
|
client.font,
|
||||||
|
line,
|
||||||
|
(w - textWidth) / 2,
|
||||||
|
h / 2,
|
||||||
|
0xFFFFFF,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearForDisconnect() {
|
||||||
|
overlayActive = false;
|
||||||
|
cooldownEndEpochMs = 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/net/respawnbackoff/CooldownSyncPayload.java
Normal file
25
src/main/java/net/respawnbackoff/CooldownSyncPayload.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package net.respawnbackoff;
|
||||||
|
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
public record CooldownSyncPayload(boolean active, long cooldownEndEpochMs) implements CustomPacketPayload {
|
||||||
|
public static final CustomPacketPayload.Type<CooldownSyncPayload> TYPE = new CustomPacketPayload.Type<>(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(RespawnBackoffMod.MOD_ID, "cooldown_sync")
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, CooldownSyncPayload> STREAM_CODEC = StreamCodec.of(
|
||||||
|
(RegistryFriendlyByteBuf buf, CooldownSyncPayload payload) -> {
|
||||||
|
buf.writeBoolean(payload.active());
|
||||||
|
buf.writeLong(payload.cooldownEndEpochMs());
|
||||||
|
},
|
||||||
|
buf -> new CooldownSyncPayload(buf.readBoolean(), buf.readLong())
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/main/java/net/respawnbackoff/RespawnBackoffCommands.java
Normal file
110
src/main/java/net/respawnbackoff/RespawnBackoffCommands.java
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package net.respawnbackoff;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||||
|
import net.minecraft.commands.CommandBuildContext;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.commands.arguments.EntityArgument;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
public final class RespawnBackoffCommands {
|
||||||
|
private RespawnBackoffCommands() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
CommandRegistrationCallback.EVENT.register(RespawnBackoffCommands::registerLiteral);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void registerLiteral(
|
||||||
|
CommandDispatcher<CommandSourceStack> dispatcher,
|
||||||
|
CommandBuildContext registryAccess,
|
||||||
|
Commands.CommandSelection environment
|
||||||
|
) {
|
||||||
|
dispatcher.register(
|
||||||
|
Commands.literal("respawnbackoff")
|
||||||
|
.requires(source -> source.hasPermission(2))
|
||||||
|
.then(
|
||||||
|
Commands.literal("reset")
|
||||||
|
.executes(ctx -> resetBackoff(ctx.getSource().getPlayerOrException(), ctx.getSource()))
|
||||||
|
.then(
|
||||||
|
Commands.argument("targets", EntityArgument.players())
|
||||||
|
.executes(ctx -> resetBackoff(EntityArgument.getPlayers(ctx, "targets"), ctx.getSource()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
Commands.literal("skip")
|
||||||
|
.executes(ctx -> skipWait(ctx.getSource().getPlayerOrException(), ctx.getSource()))
|
||||||
|
.then(
|
||||||
|
Commands.argument("targets", EntityArgument.players())
|
||||||
|
.executes(ctx -> skipWait(EntityArgument.getPlayers(ctx, "targets"), ctx.getSource()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int resetBackoff(ServerPlayer target, CommandSourceStack source) throws CommandSyntaxException {
|
||||||
|
RespawnBackoffMod.resetBackoffChain(target);
|
||||||
|
source.sendSuccess(
|
||||||
|
() -> Component.translatable("respawn_backoff.command.reset.single", target.getDisplayName()),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int resetBackoff(Collection<ServerPlayer> targets, CommandSourceStack source) {
|
||||||
|
int n = 0;
|
||||||
|
for (ServerPlayer target : targets) {
|
||||||
|
RespawnBackoffMod.resetBackoffChain(target);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (n == 1) {
|
||||||
|
source.sendSuccess(
|
||||||
|
() -> Component.translatable("respawn_backoff.command.reset.single", targets.iterator().next().getDisplayName()),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
int count = n;
|
||||||
|
source.sendSuccess(
|
||||||
|
() -> Component.translatable("respawn_backoff.command.reset.multiple", count),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int skipWait(ServerPlayer target, CommandSourceStack source) throws CommandSyntaxException {
|
||||||
|
RespawnBackoffMod.skipCooldown(target);
|
||||||
|
source.sendSuccess(
|
||||||
|
() -> Component.translatable("respawn_backoff.command.skip.single", target.getDisplayName()),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int skipWait(Collection<ServerPlayer> targets, CommandSourceStack source) {
|
||||||
|
int n = 0;
|
||||||
|
for (ServerPlayer target : targets) {
|
||||||
|
RespawnBackoffMod.skipCooldown(target);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (n == 1) {
|
||||||
|
source.sendSuccess(
|
||||||
|
() -> Component.translatable("respawn_backoff.command.skip.single", targets.iterator().next().getDisplayName()),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
int count = n;
|
||||||
|
source.sendSuccess(
|
||||||
|
() -> Component.translatable("respawn_backoff.command.skip.multiple", count),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/net/respawnbackoff/RespawnBackoffData.java
Normal file
24
src/main/java/net/respawnbackoff/RespawnBackoffData.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package net.respawnbackoff;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
public record RespawnBackoffData(
|
||||||
|
int exponent,
|
||||||
|
long lastEpochDay,
|
||||||
|
long pendingDurationMs,
|
||||||
|
long cooldownEndEpochMs
|
||||||
|
) {
|
||||||
|
public static final RespawnBackoffData DEFAULT = new RespawnBackoffData(0, 0L, 0L, 0L);
|
||||||
|
|
||||||
|
public static final Codec<RespawnBackoffData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Codec.INT.fieldOf("exponent").forGetter(RespawnBackoffData::exponent),
|
||||||
|
Codec.LONG.fieldOf("last_epoch_day").forGetter(RespawnBackoffData::lastEpochDay),
|
||||||
|
Codec.LONG.fieldOf("pending_duration_ms").forGetter(RespawnBackoffData::pendingDurationMs),
|
||||||
|
Codec.LONG.fieldOf("cooldown_end_ms").forGetter(RespawnBackoffData::cooldownEndEpochMs)
|
||||||
|
).apply(instance, RespawnBackoffData::new));
|
||||||
|
|
||||||
|
public boolean hasActiveCooldown(long nowMs) {
|
||||||
|
return cooldownEndEpochMs > nowMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/main/java/net/respawnbackoff/RespawnBackoffMod.java
Normal file
166
src/main/java/net/respawnbackoff/RespawnBackoffMod.java
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package net.respawnbackoff;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
|
||||||
|
import net.fabricmc.api.ModInitializer;
|
||||||
|
import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry;
|
||||||
|
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||||
|
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||||
|
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||||
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||||
|
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||||
|
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.GameType;
|
||||||
|
|
||||||
|
public class RespawnBackoffMod implements ModInitializer {
|
||||||
|
public static final String MOD_ID = "respawn_backoff";
|
||||||
|
|
||||||
|
public static final AttachmentType<RespawnBackoffData> RESPAWN_BACKOFF = AttachmentRegistry.create(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(MOD_ID, "state"),
|
||||||
|
builder -> builder
|
||||||
|
.persistent(RespawnBackoffData.CODEC)
|
||||||
|
.copyOnDeath()
|
||||||
|
.initializer(() -> RespawnBackoffData.DEFAULT)
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitialize() {
|
||||||
|
PayloadTypeRegistry.playS2C().register(CooldownSyncPayload.TYPE, CooldownSyncPayload.STREAM_CODEC);
|
||||||
|
RespawnBackoffCommands.register();
|
||||||
|
|
||||||
|
ServerLivingEntityEvents.AFTER_DEATH.register((entity, damageSource) -> {
|
||||||
|
if (!(entity instanceof ServerPlayer player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onPlayerDeath(player);
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> {
|
||||||
|
if (!alive) {
|
||||||
|
onAfterDeathRespawn(newPlayer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
|
||||||
|
ServerPlayer player = handler.player;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT);
|
||||||
|
if (data.hasActiveCooldown(now)) {
|
||||||
|
if (!player.isSpectator()) {
|
||||||
|
player.setGameMode(GameType.SPECTATOR);
|
||||||
|
}
|
||||||
|
RespawnBackoffNetworking.sendCooldown(player, true, data.cooldownEndEpochMs());
|
||||||
|
} else {
|
||||||
|
RespawnBackoffNetworking.sendCooldown(player, false, 0L);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerTickEvents.END_SERVER_TICK.register(server -> {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||||
|
tickPlayer(player, now);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long currentUtcEpochDay() {
|
||||||
|
return LocalDate.now(ZoneOffset.UTC).toEpochDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onPlayerDeath(ServerPlayer player) {
|
||||||
|
long today = currentUtcEpochDay();
|
||||||
|
RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT);
|
||||||
|
|
||||||
|
int exponent = data.exponent();
|
||||||
|
long lastDay = data.lastEpochDay();
|
||||||
|
if (lastDay != today) {
|
||||||
|
exponent = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cappedExp = Math.min(exponent, 6);
|
||||||
|
long waitMinutes = Math.min(1L << cappedExp, 64L);
|
||||||
|
long pendingMs = waitMinutes * 60_000L;
|
||||||
|
int nextExponent = Math.min(exponent + 1, 6);
|
||||||
|
|
||||||
|
RespawnBackoffData next = new RespawnBackoffData(nextExponent, today, pendingMs, 0L);
|
||||||
|
player.setAttached(RESPAWN_BACKOFF, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onAfterDeathRespawn(ServerPlayer player) {
|
||||||
|
RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT);
|
||||||
|
long pending = data.pendingDurationMs();
|
||||||
|
if (pending <= 0L) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long end = System.currentTimeMillis() + pending;
|
||||||
|
RespawnBackoffData next = new RespawnBackoffData(
|
||||||
|
data.exponent(),
|
||||||
|
data.lastEpochDay(),
|
||||||
|
0L,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
player.setAttached(RESPAWN_BACKOFF, next);
|
||||||
|
player.setGameMode(GameType.SPECTATOR);
|
||||||
|
RespawnBackoffNetworking.sendCooldown(player, true, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void tickPlayer(ServerPlayer player, long nowMs) {
|
||||||
|
RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT);
|
||||||
|
if (!data.hasActiveCooldown(nowMs)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nowMs < data.cooldownEndEpochMs()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCooldownAndRestore(player, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears active respawn wait and pending pre-respawn duration; restores survival if needed.
|
||||||
|
* Does not change the death-count chain (exponent / last day).
|
||||||
|
*/
|
||||||
|
public static void skipCooldown(ServerPlayer player) {
|
||||||
|
RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT);
|
||||||
|
clearCooldownAndRestore(player, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearCooldownAndRestore(ServerPlayer player, RespawnBackoffData data) {
|
||||||
|
RespawnBackoffData cleared = new RespawnBackoffData(
|
||||||
|
data.exponent(),
|
||||||
|
data.lastEpochDay(),
|
||||||
|
0L,
|
||||||
|
0L
|
||||||
|
);
|
||||||
|
player.setAttached(RESPAWN_BACKOFF, cleared);
|
||||||
|
if (player.isSpectator()) {
|
||||||
|
player.setGameMode(GameType.SURVIVAL);
|
||||||
|
}
|
||||||
|
RespawnBackoffNetworking.sendCooldown(player, false, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next death uses the minimum wait (1 minute): exponent 0 and today's UTC day recorded.
|
||||||
|
* Does not end an active on-screen countdown; use {@link #skipCooldown} for that.
|
||||||
|
*/
|
||||||
|
public static void resetBackoffChain(ServerPlayer player) {
|
||||||
|
long today = currentUtcEpochDay();
|
||||||
|
RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT);
|
||||||
|
RespawnBackoffData next = new RespawnBackoffData(
|
||||||
|
0,
|
||||||
|
today,
|
||||||
|
0L,
|
||||||
|
data.cooldownEndEpochMs()
|
||||||
|
);
|
||||||
|
player.setAttached(RESPAWN_BACKOFF, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPenaltyActive(ServerPlayer player, long nowMs) {
|
||||||
|
return player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT).hasActiveCooldown(nowMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package net.respawnbackoff;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
public final class RespawnBackoffNetworking {
|
||||||
|
private RespawnBackoffNetworking() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendCooldown(ServerPlayer player, boolean active, long cooldownEndEpochMs) {
|
||||||
|
ServerPlayNetworking.send(player, new CooldownSyncPayload(active, cooldownEndEpochMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package net.respawnbackoff.mixin;
|
||||||
|
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.GameType;
|
||||||
|
import net.respawnbackoff.RespawnBackoffMod;
|
||||||
|
|
||||||
|
@Mixin(ServerPlayer.class)
|
||||||
|
public class ServerPlayerGameModeMixin {
|
||||||
|
@Inject(method = "setGameMode", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void respawn_backoff$enforceSpectatorDuringPenalty(GameType gameType, CallbackInfoReturnable<Boolean> cir) {
|
||||||
|
if (gameType == GameType.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ServerPlayer self = (ServerPlayer) (Object) this;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (RespawnBackoffMod.isPenaltyActive(self, now)) {
|
||||||
|
cir.setReturnValue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"respawn_backoff.hud.countdown": "Respawn in %s",
|
||||||
|
"respawn_backoff.command.reset.single": "Reset respawn backoff for %s to the minimum (next death: 1 minute).",
|
||||||
|
"respawn_backoff.command.reset.multiple": "Reset respawn backoff to the minimum for %s players.",
|
||||||
|
"respawn_backoff.command.skip.single": "Skipped respawn wait for %s.",
|
||||||
|
"respawn_backoff.command.skip.multiple": "Skipped respawn wait for %s players."
|
||||||
|
}
|
||||||
27
src/main/resources/fabric.mod.json
Normal file
27
src/main/resources/fabric.mod.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "respawn_backoff",
|
||||||
|
"version": "${version}",
|
||||||
|
"name": "Respawn Backoff",
|
||||||
|
"description": "Doubling respawn delay with spectator blackout until the timer ends.",
|
||||||
|
"authors": [],
|
||||||
|
"license": "MIT",
|
||||||
|
"environment": "*",
|
||||||
|
"entrypoints": {
|
||||||
|
"main": [
|
||||||
|
"net.respawnbackoff.RespawnBackoffMod"
|
||||||
|
],
|
||||||
|
"client": [
|
||||||
|
"net.respawnbackoff.client.RespawnBackoffClient"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mixins": [
|
||||||
|
"respawn_backoff.mixins.json"
|
||||||
|
],
|
||||||
|
"depends": {
|
||||||
|
"fabricloader": ">=0.18.5",
|
||||||
|
"minecraft": "~1.21.1",
|
||||||
|
"java": ">=21",
|
||||||
|
"fabric-api": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/resources/respawn_backoff.mixins.json
Normal file
11
src/main/resources/respawn_backoff.mixins.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"package": "net.respawnbackoff.mixin",
|
||||||
|
"compatibilityLevel": "JAVA_21",
|
||||||
|
"mixins": [
|
||||||
|
"ServerPlayerGameModeMixin"
|
||||||
|
],
|
||||||
|
"injectors": {
|
||||||
|
"defaultRequire": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user