Skip to content

Commit 9bbd126

Browse files
committed
fix(knockback): prevent roof teleport exploit & improve safe knockback handling
Fixed an issue where players using fight charge could remain inside knockback regions and get teleported onto the map roof (above playable area). ### Key changes: - Reworked knockback direction to push players toward nearest region edge instead of center-based vector - Added velocity dampening system for more consistent knockback behavior - Improved vertical handling: - Separate ground vs air vertical values - Prevent excessive vertical stacking while airborne ### Teleport fallback system: - Introduced optional smart fallback teleport when knockback fails - Added continuous velocity check before teleporting (prevents false triggers) - Prevent duplicate fallback tasks using active tracking set - Improved force knockback logic with edge distance + velocity validation ### Safe location handling: - Implemented safe ground detection system: - Avoid unsafe blocks (lava, cactus, magma, etc.) - Ensure 2-block air clearance above ground - Added configurable fallback scanning from highest block - Prevent teleport if no safe location is found (optional) ### Stability improvements: - Added recursion limit for region expansion to prevent infinite loops - Improved region overlap handling during location generation - Reduced unnecessary teleports and edge-case glitches ### Config additions: - vertical / maxAirVertical - dampenVelocity / dampenFactor - useTeleport (smart fallback) - safeGroundCheck + safeHighestFallback - unsafeGroundBlocks - groundOffset - maxAttempts - cancelIfNoSafeGround Result: - Eliminates roof teleport exploit - More natural and predictable knockback - Safer teleport fallback system - Better handling of edge cases and overlapping regions
1 parent acdd2da commit 9bbd126

2 files changed

Lines changed: 363 additions & 34 deletions

File tree

Lines changed: 229 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
package com.eternalcode.combat.fight.knockback;
22

33
import com.eternalcode.combat.config.implementation.PluginConfig;
4-
import com.eternalcode.combat.region.Point;
54
import com.eternalcode.combat.region.Region;
65
import com.eternalcode.combat.region.RegionProvider;
76
import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler;
7+
import com.eternalcode.commons.scheduler.Task;
88
import io.papermc.lib.PaperLib;
9-
import java.time.Duration;
10-
import java.util.HashMap;
11-
import java.util.Map;
12-
import java.util.Optional;
13-
import java.util.UUID;
149
import org.bukkit.Location;
10+
import org.bukkit.Material;
1511
import org.bukkit.entity.Player;
1612
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
1713
import org.bukkit.util.Vector;
1814

15+
import java.time.Duration;
16+
import java.util.*;
17+
1918
public final class KnockbackService {
2019

2120
private final PluginConfig config;
2221
private final MinecraftScheduler scheduler;
2322
private final RegionProvider regionProvider;
2423

2524
private final Map<UUID, Region> insideRegion = new HashMap<>();
25+
private final Set<UUID> fallbackActive = new HashSet<>();
2626

2727
public KnockbackService(PluginConfig config, MinecraftScheduler scheduler, RegionProvider regionProvider) {
2828
this.config = config;
@@ -31,57 +31,257 @@ public KnockbackService(PluginConfig config, MinecraftScheduler scheduler, Regio
3131
}
3232

3333
public void knockbackLater(Region region, Player player, Duration duration) {
34-
this.scheduler.runLater(() -> this.knockback(region, player), duration);
34+
scheduler.runLater(() -> this.knockback(region, player), duration);
35+
}
36+
37+
public void knockback(Region region, Player player) {
38+
39+
if (player.isInsideVehicle()) {
40+
player.leaveVehicle();
41+
}
42+
43+
Location loc = player.getLocation();
44+
Vector direction = getDirectionToEdge(region, loc);
45+
46+
if (config.knockback.dampenVelocity) {
47+
player.setVelocity(player.getVelocity().multiply(config.knockback.dampenFactor));
48+
}
49+
50+
boolean onGround = Math.abs(player.getVelocity().getY()) < 0.08;
51+
52+
double y = onGround
53+
? config.knockback.vertical
54+
: Math.min(player.getVelocity().getY(), config.knockback.maxAirVertical);
55+
56+
Vector velocity = direction.multiply(config.knockback.multiplier).setY(y);
57+
58+
player.setFallDistance(0);
59+
player.setVelocity(velocity);
60+
61+
62+
if (config.knockback.useTeleport) {
63+
scheduleSmartFallback(player, region);
64+
}
3565
}
3666

3767
public void forceKnockbackLater(Player player, Region region) {
38-
if (insideRegion.containsKey(player.getUniqueId())) {
68+
UUID uuid = player.getUniqueId();
69+
70+
if (insideRegion.containsKey(uuid)) {
3971
return;
4072
}
4173

42-
insideRegion.put(player.getUniqueId(), region);
74+
insideRegion.put(uuid, region);
4375

4476
scheduler.runLater(player.getLocation(), () -> {
45-
insideRegion.remove(player.getUniqueId());
4677

47-
Location playerLocation = player.getLocation();
48-
if (!region.contains(playerLocation) && !regionProvider.isInRegion(playerLocation)) {
78+
insideRegion.remove(uuid);
79+
80+
Location loc = player.getLocation();
81+
double velocity = player.getVelocity().lengthSquared();
82+
83+
if (velocity > 0.02) {
4984
return;
5085
}
5186

52-
if (player.isInsideVehicle()) {
53-
player.leaveVehicle();
87+
if (!region.contains(loc)) {
88+
return;
89+
}
90+
91+
double distanceToEdge = getDistanceToEdge(region, loc);
92+
93+
if (distanceToEdge > 1.5) {
94+
return;
95+
}
96+
97+
if (fallbackActive.contains(uuid)) {
98+
return;
99+
}
100+
101+
Location generated = generate(
102+
player.getLocation(),
103+
Point2D.from(region.getMin()),
104+
Point2D.from(region.getMax()),
105+
0
106+
);
107+
108+
Location safe = makeSafe(generated);
109+
if (safe == null || safe.getWorld() == null) {
110+
return;
54111
}
55112

56-
Location location = generate(playerLocation, Point2D.from(region.getMin()), Point2D.from(region.getMax()));
113+
PaperLib.teleportAsync(player, safe.clone(), TeleportCause.PLUGIN);
57114

58-
PaperLib.teleportAsync(player, location, TeleportCause.PLUGIN);
59-
}, this.config.knockback.forceDelay);
115+
}, config.knockback.forceDelay);
60116
}
61117

62-
private Location generate(Location playerLocation, Point2D minX, Point2D maxX) {
118+
private void scheduleSmartFallback(Player player, Region region) {
119+
UUID uuid = player.getUniqueId();
120+
121+
if (fallbackActive.contains(uuid)) {
122+
return;
123+
}
124+
125+
fallbackActive.add(uuid);
126+
127+
final Task[] taskRef = new Task[1];
128+
129+
taskRef[0] = scheduler.timer(() -> {
130+
131+
Location check = player.getLocation();
132+
double velocity = player.getVelocity().lengthSquared();
133+
134+
if (!region.contains(check)) {
135+
fallbackActive.remove(uuid);
136+
taskRef[0].cancel();
137+
return;
138+
}
139+
140+
if (velocity > 0.02) {
141+
return;
142+
}
143+
144+
Location generated = generate(
145+
player.getLocation(),
146+
Point2D.from(region.getMin()),
147+
Point2D.from(region.getMax()),
148+
0
149+
);
150+
151+
Location safe = makeSafe(generated);
152+
if (safe == null || safe.getWorld() == null) {
153+
fallbackActive.remove(uuid);
154+
taskRef[0].cancel();
155+
return;
156+
}
157+
158+
PaperLib.teleportAsync(player, safe.clone(), TeleportCause.PLUGIN);
159+
160+
fallbackActive.remove(uuid);
161+
taskRef[0].cancel();
162+
163+
},
164+
Duration.ofMillis(100),
165+
Duration.ofMillis(100));
166+
}
167+
168+
private Location makeSafe(Location loc) {
169+
if (loc == null || loc.getWorld() == null) return loc;
170+
171+
return config.knockback.safeGroundCheck
172+
? findSafeGround(loc)
173+
: loc.getWorld().getHighestBlockAt(loc).getLocation().add(0, config.knockback.groundOffset, 0);
174+
}
175+
176+
private Location findSafeGround(Location loc) {
177+
178+
if (loc.getWorld() == null) return loc;
179+
180+
Location check = loc.clone();
181+
int minY = loc.getWorld().getMinHeight();
182+
183+
for (int y = check.getBlockY(); y > minY; y--) {
184+
check.setY(y);
185+
186+
Material type = check.getBlock().getType();
187+
Material above = check.clone().add(0, 1, 0).getBlock().getType();
188+
Material above2 = check.clone().add(0, 2, 0).getBlock().getType();
189+
190+
if (type.isSolid()
191+
&& !config.knockback.unsafeGroundBlocks.contains(type)
192+
&& above.isAir()
193+
&& above2.isAir()) {
194+
195+
return check.clone().add(0, config.knockback.groundOffset, 0);
196+
}
197+
}
198+
199+
return getSafeHighest(loc);
200+
}
201+
202+
private Location getSafeHighest(Location loc) {
203+
if (loc == null || loc.getWorld() == null) return loc;
204+
205+
if (!config.knockback.safeHighestFallback) {
206+
return loc.getWorld()
207+
.getHighestBlockAt(loc)
208+
.getLocation()
209+
.add(0, config.knockback.groundOffset, 0);
210+
}
211+
212+
Location highest = loc.getWorld().getHighestBlockAt(loc).getLocation();
213+
int minY = loc.getWorld().getMinHeight();
214+
215+
int startY = highest.getBlockY();
216+
int maxScan = config.knockback.safeHighestMaxScan;
217+
218+
int endY = (maxScan < 0)
219+
? minY
220+
: Math.max(minY, startY - maxScan);
221+
222+
for (int y = startY; y > endY; y--) {
223+
highest.setY(y);
224+
225+
Material type = highest.getBlock().getType();
226+
Material above = highest.clone().add(0, 1, 0).getBlock().getType();
227+
Material above2 = highest.clone().add(0, 2, 0).getBlock().getType();
228+
229+
if (type.isSolid()
230+
&& !config.knockback.unsafeGroundBlocks.contains(type)
231+
&& above.isAir()
232+
&& above2.isAir()) {
233+
234+
return highest.clone().add(0, config.knockback.groundOffset, 0);
235+
}
236+
}
237+
238+
return config.knockback.cancelIfNoSafeGround ? null : loc;
239+
}
240+
241+
private Location generate(Location playerLocation, Point2D minX, Point2D maxX, int attempts) {
242+
if (attempts >= config.knockback.maxAttempts) {
243+
return playerLocation;
244+
}
245+
63246
Location location = KnockbackOutsideRegionGenerator.generate(minX, maxX, playerLocation);
247+
64248
Optional<Region> otherRegion = regionProvider.getRegion(location);
65249
if (otherRegion.isPresent()) {
250+
66251
Region region = otherRegion.get();
67-
return generate(playerLocation, minX.min(region.getMin()), maxX.max(region.getMax()));
252+
253+
return generate(
254+
playerLocation,
255+
minX.min(region.getMin()),
256+
maxX.max(region.getMax()),
257+
attempts + 1
258+
);
68259
}
69260

70261
return location;
71262
}
72263

73-
public void knockback(Region region, Player player) {
74-
if (player.isInsideVehicle()) {
75-
player.leaveVehicle();
76-
}
264+
private Vector getDirectionToEdge(Region region, Location loc) {
265+
double dxMin = loc.getX() - region.getMin().getX();
266+
double dxMax = region.getMax().getX() - loc.getX();
267+
double dzMin = loc.getZ() - region.getMin().getZ();
268+
double dzMax = region.getMax().getZ() - loc.getZ();
269+
270+
double min = Math.min(Math.min(dxMin, dxMax), Math.min(dzMin, dzMax));
77271

78-
Point point = region.getCenter();
79-
Location subtract = player.getLocation().subtract(point.x(), 0, point.z());
272+
if (Math.abs(min - dxMin) < 1e-6) return new Vector(-1, 0, 0);
273+
if (Math.abs(min - dxMax) < 1e-6) return new Vector(1, 0, 0);
274+
if (Math.abs(min - dzMin) < 1e-6) return new Vector(0, 0, -1);
275+
276+
return new Vector(0, 0, 1);
277+
}
80278

81-
Vector knockbackVector = new Vector(subtract.getX(), 0, subtract.getZ()).normalize();
82-
double multiplier = this.config.knockback.multiplier;
83-
Vector configuredVector = new Vector(multiplier, 0.5, multiplier);
279+
private double getDistanceToEdge(Region region, Location loc) {
280+
double dxMin = loc.getX() - region.getMin().getX();
281+
double dxMax = region.getMax().getX() - loc.getX();
282+
double dzMin = loc.getZ() - region.getMin().getZ();
283+
double dzMax = region.getMax().getZ() - loc.getZ();
84284

85-
player.setVelocity(knockbackVector.multiply(configuredVector));
285+
return Math.min(Math.min(dxMin, dxMax), Math.min(dzMin, dzMax));
86286
}
87287
}

0 commit comments

Comments
 (0)