feat(vigilante): add arena boss succession
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class ArenaGeometryTest {
|
||||
private final ArenaLocation center = new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F);
|
||||
|
||||
@Test
|
||||
void circleUsesHorizontalDistanceAndWorld() {
|
||||
assertTrue(ArenaGeometry.contains(center, 10.0, "world", 16.0, 100.0, 3.0));
|
||||
assertFalse(ArenaGeometry.contains(center, 10.0, "world", 21.0, 64.0, -5.0));
|
||||
assertFalse(ArenaGeometry.contains(center, 10.0, "nether", 10.0, 64.0, -5.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outsidePointProjectsBeyondNearestBoundary() {
|
||||
ArenaPoint point = ArenaGeometry.outside(center, 10.0, 10.0, 64.0, -5.0);
|
||||
|
||||
assertEquals(21.0, point.x(), 0.001);
|
||||
assertEquals(64.0, point.y(), 0.001);
|
||||
assertEquals(-5.0, point.z(), 0.001);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class BukkitConfigArenaLocationStoreTest {
|
||||
@Test
|
||||
void arenaLocationRoundTripsThroughPluginConfiguration() {
|
||||
JavaPlugin plugin = mock(JavaPlugin.class);
|
||||
YamlConfiguration config = new YamlConfiguration();
|
||||
when(plugin.getConfig()).thenReturn(config);
|
||||
BukkitConfigArenaLocationStore store = new BukkitConfigArenaLocationStore(plugin);
|
||||
ArenaLocation expected = new ArenaLocation(
|
||||
"arena_world", 12.5, 70.0, -4.5, 90.0F, 5.0F
|
||||
);
|
||||
|
||||
store.set(expected);
|
||||
|
||||
assertEquals(expected, store.location().orElseThrow());
|
||||
verify(plugin).saveConfig();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@ final class PluginSettingsTest {
|
||||
assertEquals(Duration.ofHours(1), settings.fixerCooldown());
|
||||
assertEquals(Duration.ofMinutes(10), settings.fixerEffectDuration());
|
||||
assertEquals(Duration.ofHours(24), settings.rosterIntelligenceCooldown());
|
||||
assertEquals(10.0, settings.vigilanteArenaRadiusBlocks());
|
||||
assertEquals(80.0, settings.vigilanteBossHealth());
|
||||
assertEquals(8.0, settings.vigilanteBossDamage());
|
||||
assertEquals(10.0, settings.vigilanteBossArmor());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -67,5 +71,17 @@ final class PluginSettingsTest {
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("role-inactivity-seconds", -1))
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("vigilante-arena-radius-blocks", 0))
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("vigilante-boss-health", Double.NaN))
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("vigilante-boss-armor", 31))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ final class RoleMaintenanceServiceTest {
|
||||
);
|
||||
|
||||
@Test
|
||||
void onlinePendingVigilanteConfirmsWithoutConflictingWithTyrant() {
|
||||
void legacyPendingVigilanteIsClearedForArenaSuccession() {
|
||||
GameState game = game(
|
||||
Optional.of(TYRANT), Optional.empty(), Optional.empty(),
|
||||
Optional.of(new PendingSelection(CANDIDATE, NOW.plusSeconds(60)))
|
||||
@@ -43,12 +43,12 @@ final class RoleMaintenanceServiceTest {
|
||||
|
||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT, CANDIDATE), NOW);
|
||||
|
||||
assertEquals(Optional.of(CANDIDATE), maintained.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), maintained.game().pendingVigilante());
|
||||
}
|
||||
|
||||
@Test
|
||||
void expiredPendingSelectionRerollsAwayFromPreviousCandidate() {
|
||||
void expiredLegacyPendingVigilanteOpensArenaInsteadOfRerolling() {
|
||||
GameState game = game(
|
||||
Optional.of(TYRANT), Optional.empty(), Optional.empty(),
|
||||
Optional.of(new PendingSelection(CANDIDATE, NOW.minusSeconds(1)))
|
||||
@@ -59,12 +59,12 @@ final class RoleMaintenanceServiceTest {
|
||||
|
||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT), NOW);
|
||||
|
||||
assertEquals(OTHER,
|
||||
maintained.game().pendingVigilante().orElseThrow().candidateId());
|
||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), maintained.game().pendingVigilante());
|
||||
}
|
||||
|
||||
@Test
|
||||
void inactiveVigilanteIsRerolledAndFollowersAreCleared() {
|
||||
void inactiveVigilanteOpensArenaAndFollowersAreCleared() {
|
||||
PlayerState follower = withFollower(
|
||||
withLogin(active(FOLLOWER), NOW.minus(Duration.ofHours(25))),
|
||||
VIGILANTE
|
||||
@@ -80,7 +80,7 @@ final class RoleMaintenanceServiceTest {
|
||||
|
||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT, OTHER), NOW);
|
||||
|
||||
assertEquals(Optional.of(OTHER), maintained.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), maintained.players().get(FOLLOWER).followerOf());
|
||||
assertEquals(Optional.of(TYRANT), maintained.game().tyrantId());
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleRelinquishmentServiceTest {
|
||||
@Test
|
||||
void vigilanteCanRelinquishAndTriggerNormalReroll() {
|
||||
void vigilanteCanRelinquishAndOpenArenaChallenge() {
|
||||
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilante = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
UUID candidate = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
@@ -49,7 +49,8 @@ final class RoleRelinquishmentServiceTest {
|
||||
);
|
||||
|
||||
assertEquals(RelinquishmentStatus.RELINQUISHED, result.status());
|
||||
assertEquals(Optional.of(candidate), result.state().game().vigilanteId());
|
||||
assertEquals(Optional.empty(), result.state().game().vigilanteId());
|
||||
assertEquals(Optional.empty(), result.state().game().pendingVigilante());
|
||||
}
|
||||
|
||||
private static PlayerState active(UUID id, Instant now) {
|
||||
|
||||
@@ -14,8 +14,11 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantAdminCommandTest {
|
||||
@@ -59,6 +62,30 @@ final class TyrantAdminCommandTest {
|
||||
verify(effects).suppressAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void playerCanPersistArenaAtCurrentLocation() {
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
Player sender = mock(Player.class);
|
||||
World world = mock(World.class);
|
||||
when(sender.hasPermission("spigottyrant.admin")).thenReturn(true);
|
||||
when(world.getName()).thenReturn("arena_world");
|
||||
when(sender.getLocation()).thenReturn(new Location(world, 12.5, 70.0, -4.5, 90.0F, 0.0F));
|
||||
ArenaLocationStore store = mock(ArenaLocationStore.class);
|
||||
TyrantAdminCommand command = new TyrantAdminCommand(
|
||||
manager, new GameLifecycleService(), mock(OnlinePlayerDirectory.class),
|
||||
mock(ManagedRoleEffects.class),
|
||||
Clock.fixed(Instant.parse("2026-08-14T12:00:00Z"), ZoneOffset.UTC), store
|
||||
);
|
||||
|
||||
command.onCommand(sender, mock(Command.class), "tyrantadmin", new String[] {"arena", "set"});
|
||||
|
||||
verify(store).set(org.mockito.ArgumentMatchers.argThat(location ->
|
||||
location.worldName().equals("arena_world")
|
||||
&& location.x() == 12.5 && location.y() == 70.0 && location.z() == -4.5
|
||||
));
|
||||
verify(sender).sendMessage(contains("Vigilante arena set"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetRequiresExplicitConfirmation() {
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
|
||||
@@ -34,7 +34,9 @@ final class TyrantAdminTabCompleterTest {
|
||||
));
|
||||
assertTrue(completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {""}
|
||||
).containsAll(java.util.List.of("status", "start", "pause", "resume", "reset")));
|
||||
).containsAll(java.util.List.of(
|
||||
"status", "start", "pause", "resume", "reset", "arena"
|
||||
)));
|
||||
assertEquals(java.util.List.of("Alpha", "Beta"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"start", ""}
|
||||
));
|
||||
@@ -44,6 +46,9 @@ final class TyrantAdminTabCompleterTest {
|
||||
assertEquals(java.util.List.of("confirm"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"reset", "c"}
|
||||
));
|
||||
assertEquals(java.util.List.of("set", "status"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"arena", "s"}
|
||||
));
|
||||
}
|
||||
|
||||
private static Player player(UUID id, String name) {
|
||||
|
||||
@@ -37,7 +37,8 @@ final class TyrantSuccessionServiceTest {
|
||||
assertEquals(0, after.game().tyrantLevel());
|
||||
assertEquals(1, after.game().unspentChoices());
|
||||
assertEquals(Set.of(), after.game().purchases());
|
||||
assertNotEquals(after.game().tyrantId(), after.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), after.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), after.game().pendingVigilante());
|
||||
for (PlayerState player : after.players().values()) {
|
||||
assertEquals(TyrantClass.NONE, player.tyrantClass());
|
||||
assertEquals(Optional.empty(), player.followerOf());
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteArenaBarrierTest {
|
||||
private static final ArenaLocation CENTER = new ArenaLocation(
|
||||
"world", 0.0, 64.0, 0.0, 0.0F, 0.0F
|
||||
);
|
||||
|
||||
@Test
|
||||
void doesNotReplaceExistingNonAirBlocks() {
|
||||
World world = mock(World.class);
|
||||
Block solid = mock(Block.class);
|
||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(solid);
|
||||
when(solid.getLocation()).thenReturn(new Location(world, 1.0, 64.0, 0.0));
|
||||
when(solid.getType()).thenReturn(Material.CHEST);
|
||||
|
||||
new VigilanteArenaBarrier().build(world, CENTER, 10.0);
|
||||
|
||||
verify(solid, never()).setType(Material.BARRIER, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoresAirReplacedByTemporaryBarrier() {
|
||||
World world = mock(World.class);
|
||||
Block air = mock(Block.class);
|
||||
BlockData original = mock(BlockData.class);
|
||||
BlockData snapshot = mock(BlockData.class);
|
||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(air);
|
||||
when(world.getBlockAt(any(Location.class))).thenReturn(air);
|
||||
when(air.getLocation()).thenReturn(new Location(world, 1.0, 64.0, 0.0));
|
||||
when(air.getType()).thenReturn(Material.AIR);
|
||||
when(air.getBlockData()).thenReturn(original);
|
||||
when(original.clone()).thenReturn(snapshot);
|
||||
VigilanteArenaBarrier barrier = new VigilanteArenaBarrier();
|
||||
|
||||
barrier.build(world, CENTER, 10.0);
|
||||
barrier.clear();
|
||||
|
||||
verify(air, atLeastOnce()).setType(Material.BARRIER, false);
|
||||
verify(air).setBlockData(snapshot, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteArenaControllerTest {
|
||||
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
@Test
|
||||
void optedInPlayerJoiningDuringVacancyReceivesArenaCoordinates() {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(PLAYER);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(openGame());
|
||||
when(manager.players()).thenReturn(Map.of(PLAYER, PlayerState.newPlayer(PLAYER, "Player")));
|
||||
ArenaLocationStore locations = mock(ArenaLocationStore.class);
|
||||
when(locations.location()).thenReturn(Optional.of(
|
||||
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
||||
));
|
||||
VigilanteArenaController controller = new VigilanteArenaController(
|
||||
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
||||
);
|
||||
|
||||
controller.onJoin(mockJoin(player));
|
||||
|
||||
verify(player).sendMessage(contains("world at 10, 64, -5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void optedOutPlayerDoesNotReceiveVacancyNotice() {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(PLAYER);
|
||||
PlayerState optedOut = new PlayerState(
|
||||
PLAYER, "Player", Optional.empty(), Optional.of(Instant.parse("2026-08-24T00:00:00Z")),
|
||||
TyrantClass.NONE, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(openGame());
|
||||
when(manager.players()).thenReturn(Map.of(PLAYER, optedOut));
|
||||
ArenaLocationStore locations = mock(ArenaLocationStore.class);
|
||||
when(locations.location()).thenReturn(Optional.of(
|
||||
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
||||
));
|
||||
VigilanteArenaController controller = new VigilanteArenaController(
|
||||
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
||||
);
|
||||
|
||||
controller.onJoin(mockJoin(player));
|
||||
|
||||
verify(player, never()).sendMessage(org.mockito.ArgumentMatchers.anyString());
|
||||
}
|
||||
|
||||
private static PlayerJoinEvent mockJoin(Player player) {
|
||||
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
|
||||
when(event.getPlayer()).thenReturn(player);
|
||||
return event;
|
||||
}
|
||||
|
||||
private static GameState openGame() {
|
||||
return new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteArenaPolicyTest {
|
||||
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
@Test
|
||||
void opensOnlyForRunningGameWithTyrantAndVacantVigilante() {
|
||||
assertTrue(VigilanteArenaPolicy.isOpen(game(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty()
|
||||
)));
|
||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
||||
GameLifecycle.PAUSED, Optional.of(TYRANT), Optional.empty()
|
||||
)));
|
||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty()
|
||||
)));
|
||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.of(PLAYER)
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyOptedInNonTyrantMayChallenge() {
|
||||
GameState game = game(GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty());
|
||||
PlayerState eligible = PlayerState.newPlayer(PLAYER, "Player");
|
||||
PlayerState optedOut = new PlayerState(
|
||||
PLAYER, "Player", Optional.empty(), Optional.of(Instant.parse("2026-08-24T12:00:00Z")),
|
||||
TyrantClass.NONE, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||
);
|
||||
|
||||
assertTrue(VigilanteArenaPolicy.isEligible(game, eligible));
|
||||
assertFalse(VigilanteArenaPolicy.isEligible(game, optedOut));
|
||||
assertFalse(VigilanteArenaPolicy.isEligible(game,
|
||||
PlayerState.newPlayer(TYRANT, "Tyrant")));
|
||||
}
|
||||
|
||||
private static GameState game(
|
||||
GameLifecycle lifecycle,
|
||||
Optional<UUID> tyrant,
|
||||
Optional<UUID> vigilante
|
||||
) {
|
||||
return new GameState(
|
||||
lifecycle, tyrant, vigilante, Optional.empty(), Optional.empty(),
|
||||
lifecycle == GameLifecycle.PAUSED
|
||||
? Optional.of(Instant.parse("2026-08-23T12:00:00Z")) : Optional.empty(),
|
||||
Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteArenaSuccessionServiceTest {
|
||||
@Test
|
||||
void eligibleBossWinnerClaimsVacantVigilanteRole() {
|
||||
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID winner = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrant), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
PersistentState before = new PersistentState(
|
||||
game, Map.of(winner, PlayerState.newPlayer(winner, "Winner"))
|
||||
);
|
||||
|
||||
LifecycleState claimed = new VigilanteArenaSuccessionService().claim(before, winner);
|
||||
|
||||
assertEquals(Optional.of(winner), claimed.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), claimed.game().pendingVigilante());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user