Skip to content

Commit dfafccd

Browse files
committed
fix: Select a correct Y in 'server' spawn mode, fallback on default spawnLocation if none available
1 parent 0b74c5d commit dfafccd

2 files changed

Lines changed: 153 additions & 6 deletions

File tree

authme-core/src/main/java/fr/xephi/authme/settings/SpawnLoader.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.bukkit.GameRule;
1515
import org.bukkit.Location;
1616
import org.bukkit.World;
17+
import org.bukkit.block.Block;
1718
import org.bukkit.configuration.file.FileConfiguration;
1819
import org.bukkit.configuration.file.YamlConfiguration;
1920
import org.bukkit.entity.Player;
@@ -262,8 +263,48 @@ private Location getServerSpawnLocation(World world) {
262263
int dz = (int) (Math.random() * (radius * 2 + 1)) - radius;
263264
int x = (int) worldSpawn.getX() + dx;
264265
int z = (int) worldSpawn.getZ() + dz;
265-
int y = world.getHighestBlockYAt(x, z) + 1;
266-
return new Location(world, x + 0.5, y, z + 0.5, worldSpawn.getYaw(), worldSpawn.getPitch());
266+
Integer y = getSafeSpawnY(world, x, z, worldSpawn.getBlockY());
267+
if (y == null) {
268+
return worldSpawn;
269+
}
270+
double spawnX = x + 0.5;
271+
double spawnZ = z + 0.5;
272+
float yaw = (float) Math.toDegrees(Math.atan2(-(worldSpawn.getX() - spawnX), worldSpawn.getZ() - spawnZ));
273+
return new Location(world, spawnX, y, spawnZ, yaw, 0.0f);
274+
}
275+
276+
/**
277+
* Returns a safe Y coordinate for spawning at (x, z), searching near the world spawn's Y.
278+
* Checks if the foot and head blocks are passable at the base Y; if the foot is already
279+
* passable the search goes downward (looking for a floor), otherwise upward (looking for a gap).
280+
* A margin of 10 blocks is applied in each direction. Returns {@code null} if no safe spot
281+
* is found, in which case the caller should fall back to the exact world spawn location.
282+
*/
283+
private Integer getSafeSpawnY(World world, int x, int z, int baseY) {
284+
if (isPassable(world, x, baseY, z) && isPassable(world, x, baseY + 1, z)) {
285+
return baseY;
286+
}
287+
int margin = 10;
288+
if (world.getBlockAt(x, baseY, z).isPassable()) {
289+
for (int dy = 1; dy <= margin; dy++) {
290+
int y = baseY - dy;
291+
if (isPassable(world, x, y, z) && isPassable(world, x, y + 1, z)) {
292+
return y;
293+
}
294+
}
295+
} else {
296+
for (int dy = 1; dy <= margin; dy++) {
297+
int y = baseY + dy;
298+
if (isPassable(world, x, y, z) && isPassable(world, x, y + 1, z)) {
299+
return y;
300+
}
301+
}
302+
}
303+
return null;
304+
}
305+
306+
private boolean isPassable(World world, int x, int y, int z) {
307+
return world.getBlockAt(x, y, z).isPassable();
267308
}
268309

269310
/**

authme-core/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
import org.bukkit.Bukkit;
1414
import org.bukkit.Location;
1515
import org.bukkit.World;
16+
import org.bukkit.block.Block;
1617
import org.bukkit.entity.Player;
18+
import org.mockito.ArgumentMatchers;
1719
import org.bukkit.configuration.file.YamlConfiguration;
1820
import org.junit.jupiter.api.Test;
1921
import fr.xephi.authme.TempFolder;
@@ -27,6 +29,7 @@
2729
import org.bukkit.GameRule;
2830

2931
import static org.hamcrest.Matchers.both;
32+
import static org.hamcrest.Matchers.closeTo;
3033
import static org.hamcrest.Matchers.equalTo;
3134
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
3235
import static org.hamcrest.Matchers.lessThanOrEqualTo;
@@ -147,24 +150,127 @@ public void shouldReturnExactWorldSpawnForServerPriorityWithZeroRadius() {
147150
}
148151

149152
@Test
150-
public void shouldReturnLocationWithinRadiusForServerPriority() {
153+
public void shouldReturnExactYAndFaceTowardCenterWhenBaseYIsAlreadySafe() {
151154
// given
152155
given(settings.getProperty(RestrictionSettings.SPAWN_PRIORITY)).willReturn("server");
153156
spawnLoader.reload();
154157

155158
World world = mock(World.class);
159+
// worldSpawn at (100, 64, 200); place the result due east (+X) so the expected yaw is -90°
156160
Location worldSpawn = new Location(world, 100.0, 64.0, 200.0);
157161
given(world.getSpawnLocation()).willReturn(worldSpawn);
162+
// radius=0 forces dx=dz=0 → exact worldSpawn → returned as-is (no yaw recalculation)
158163
given(world.getGameRuleValue(GameRule.SPAWN_RADIUS)).willReturn(10);
159-
given(world.getHighestBlockYAt(org.mockito.ArgumentMatchers.anyInt(), org.mockito.ArgumentMatchers.anyInt())).willReturn(63);
164+
165+
Block passable = mock(Block.class);
166+
given(passable.isPassable()).willReturn(true);
167+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(64), ArgumentMatchers.anyInt()))
168+
.willReturn(passable);
169+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(65), ArgumentMatchers.anyInt()))
170+
.willReturn(passable);
160171

161172
// when
162173
Location result = spawnLoader.getSpawnLocation(world);
163174

164-
// then
175+
// then – position within radius, Y unchanged, pitch = 0
165176
assertThat(result.getX(), both(greaterThanOrEqualTo(90.5)).and(lessThanOrEqualTo(110.5)));
166177
assertThat(result.getZ(), both(greaterThanOrEqualTo(190.5)).and(lessThanOrEqualTo(210.5)));
167-
assertThat(result.getY(), equalTo(64.0)); // highestBlockYAt(63) + 1 = 64
178+
assertThat(result.getY(), equalTo(64.0));
179+
assertThat((double) result.getPitch(), closeTo(0.0, 0.001));
180+
// yaw must point from the result position toward worldSpawn (100, 200)
181+
double expectedYaw = Math.toDegrees(Math.atan2(-(100.0 - result.getX()), 200.0 - result.getZ()));
182+
assertThat((double) result.getYaw(), closeTo(expectedYaw, 0.001));
183+
}
184+
185+
@Test
186+
public void shouldSearchDownwardWhenBaseYIsInAVoid() {
187+
// given – foot at baseY is passable but head (baseY+1) is solid (1-block-high gap, e.g. near Nether ceiling)
188+
given(settings.getProperty(RestrictionSettings.SPAWN_PRIORITY)).willReturn("server");
189+
spawnLoader.reload();
190+
191+
World world = mock(World.class);
192+
Location worldSpawn = new Location(world, 0.0, 70.0, 0.0);
193+
given(world.getSpawnLocation()).willReturn(worldSpawn);
194+
given(world.getGameRuleValue(GameRule.SPAWN_RADIUS)).willReturn(5);
195+
196+
Block passable = mock(Block.class);
197+
given(passable.isPassable()).willReturn(true);
198+
Block solid = mock(Block.class);
199+
given(solid.isPassable()).willReturn(false);
200+
201+
// y=70 passable, y=71 solid → initial check fails; foot is passable → search downward
202+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(70), ArgumentMatchers.anyInt()))
203+
.willReturn(passable);
204+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(71), ArgumentMatchers.anyInt()))
205+
.willReturn(solid);
206+
// y=69 passable → isPassable(69) && isPassable(70) = true → safe at y=69
207+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(69), ArgumentMatchers.anyInt()))
208+
.willReturn(passable);
209+
210+
// when
211+
Location result = spawnLoader.getSpawnLocation(world);
212+
213+
// then – first clear 2-block gap found going downward is at y=69
214+
assertThat(result.getY(), equalTo(69.0));
215+
}
216+
217+
@Test
218+
public void shouldSearchUpwardWhenBaseYIsInsideASolidBlock() {
219+
// given – baseY is inside a solid block (e.g. underground)
220+
given(settings.getProperty(RestrictionSettings.SPAWN_PRIORITY)).willReturn("server");
221+
spawnLoader.reload();
222+
223+
World world = mock(World.class);
224+
Location worldSpawn = new Location(world, 0.0, 60.0, 0.0);
225+
given(world.getSpawnLocation()).willReturn(worldSpawn);
226+
given(world.getGameRuleValue(GameRule.SPAWN_RADIUS)).willReturn(5);
227+
228+
Block passable = mock(Block.class);
229+
given(passable.isPassable()).willReturn(true);
230+
Block solid = mock(Block.class);
231+
given(solid.isPassable()).willReturn(false);
232+
233+
// y=60 is solid → search upward
234+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(60), ArgumentMatchers.anyInt()))
235+
.willReturn(solid);
236+
// y=61 solid, y=62 solid, y=63 passable, y=64 passable → safe at y=63
237+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(61), ArgumentMatchers.anyInt()))
238+
.willReturn(solid);
239+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(62), ArgumentMatchers.anyInt()))
240+
.willReturn(solid);
241+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(63), ArgumentMatchers.anyInt()))
242+
.willReturn(passable);
243+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(64), ArgumentMatchers.anyInt()))
244+
.willReturn(passable);
245+
246+
// when
247+
Location result = spawnLoader.getSpawnLocation(world);
248+
249+
// then
250+
assertThat(result.getY(), equalTo(63.0));
251+
}
252+
253+
@Test
254+
public void shouldFallbackToExactWorldSpawnWhenNoSafeSpotFoundWithinMargin() {
255+
// given
256+
given(settings.getProperty(RestrictionSettings.SPAWN_PRIORITY)).willReturn("server");
257+
spawnLoader.reload();
258+
259+
World world = mock(World.class);
260+
Location worldSpawn = new Location(world, 12.0, 64.0, -34.0);
261+
given(world.getSpawnLocation()).willReturn(worldSpawn);
262+
given(world.getGameRuleValue(GameRule.SPAWN_RADIUS)).willReturn(5);
263+
264+
Block solid = mock(Block.class);
265+
given(solid.isPassable()).willReturn(false);
266+
given(world.getBlockAt(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
267+
.willReturn(solid);
268+
269+
// when
270+
Location result = spawnLoader.getSpawnLocation(world);
271+
272+
// then – falls back to exact worldSpawn (X/Y/Z unchanged)
273+
assertThat(result, equalTo(worldSpawn));
168274
}
169275

170276
@Test

0 commit comments

Comments
 (0)