Skip to content

Commit c5bb1b3

Browse files
committed
update: add per-player spawner limit functionality and related commands
1 parent 1425244 commit c5bb1b3

13 files changed

Lines changed: 761 additions & 55 deletions

File tree

README.md

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,122 @@
1-
# 🎯 SSAddon SpawnerLimiter
1+
# SSAddon SpawnerLimiter
22

3-
SSAddon (SmartSpawner Addon) that limits spawner placement per chunk based on spawner stacks, with Folia support!
3+
SSAddon (SmartSpawner Addon) that limits spawner placement per chunk and per player based on spawner stacks, with Folia support!
44

55
[![Minecraft](https://img.shields.io/badge/Minecraft-1.21.4+-green.svg)](https://www.minecraft.net/)
66
[![Paper](https://img.shields.io/badge/Server-Paper%20%7C%20Folia-blue.svg)](https://papermc.io/)
77
[![Java](https://img.shields.io/badge/Java-21+-orange.svg)](https://www.oracle.com/java/)
88

9-
## 📋 Requirements
9+
## Requirements
1010

1111
- **Minecraft**: 1.21.4+
1212
- **Server**: Paper or Folia
1313
- **Java**: 21+
1414
- **SmartSpawner**: 1.5.7.1+
1515

16-
## 📦 Installation
16+
## Installation
1717

1818
1. Install [SmartSpawner](https://modrinth.com/plugin/smart-spawner-plugin) plugin
1919
2. Download SSASpawnerLimiter
2020
3. Place the `.jar` file in your `plugins` folder
2121
4. Restart the server
2222
5. Configure settings in `plugins/SSASpawnerLimiter/config.yml`
2323

24-
## 🎮 Commands
24+
## Commands
2525

2626
| Command | Description | Aliases |
2727
|---------|-------------|---------|
2828
| `/ssaspawnerlimiter reload` | Reload plugin configuration | `/ssalimiter reload` |
2929
| `/ssaspawnerlimiter info` | Check spawner count in current chunk | `/ssalimiter info` |
30-
| `/ssaspawnerlimiter check <player>` | Check spawner limit for a specific player | `/ssalimiter check <player>` |
30+
| `/ssaspawnerlimiter check <player>` | Check spawner limit for player's chunk | `/ssalimiter check <player>` |
31+
| `/ssaspawnerlimiter checkplayer <player>` | Check player's global spawner count | `/ssalimiter checkplayer <player>` |
3132
| `/ssaspawnerlimiter stats` | View plugin statistics | `/ssalimiter stats` |
3233

33-
## 🔐 Permissions
34+
## Permissions
3435

36+
### Bypass Permissions
3537
| Permission | Description | Default |
3638
|------------|-------------|---------|
3739
| `ssaspawnerlimiter.bypass` | Bypass spawner chunk limit | `false` |
40+
| `ssaspawnerlimiter.perplayer.bypass` | Bypass per-player spawner limit (unlimited) | `false` |
41+
42+
### Per-Player Limit Tiers
43+
You can create **custom limit tiers** using the permission pattern: `ssaspawnerlimiter.perplayer.<number>`
44+
45+
**Examples:**
46+
- `ssaspawnerlimiter.perplayer.1500` → Allows 1500 spawners globally
47+
- `ssaspawnerlimiter.perplayer.2000` → Allows 2000 spawners globally
48+
- `ssaspawnerlimiter.perplayer.5000` → Allows 5000 spawners globally
49+
- `ssaspawnerlimiter.perplayer.10000` → Allows 10000 spawners globally
50+
- Any number you want!
51+
52+
> **Note:** Players with multiple tier permissions will get the highest value. Default limit is configured in `config.yml` as `max_spawners_per_player`.
53+
54+
### Command Permissions
55+
| Permission | Description | Default |
56+
|------------|-------------|---------|
3857
| `ssaspawnerlimiter.command.use` | Base permission for all commands | `op` |
3958
| `ssaspawnerlimiter.command.reload` | Use reload command | `op` |
4059
| `ssaspawnerlimiter.command.info` | Use info command | `op` |
4160
| `ssaspawnerlimiter.command.check` | Use check command | `op` |
61+
| `ssaspawnerlimiter.command.checkplayer` | Use checkplayer command | `op` |
4262
| `ssaspawnerlimiter.command.stats` | Use stats command | `op` |
4363

44-
## 📖 How It Works
64+
## How It Works
65+
66+
SSASpawnerLimiter provides **two independent limit systems**:
4567

46-
SSASpawnerLimiter counts **spawner stacks** instead of just spawner blocks:
68+
### Chunk-Based Limit
69+
Limits spawner stacks per chunk to prevent chunk overloading:
4770
- 1 spawner block with 64 stacks = **64 count**
4871
- 1 spawner block with 6 stacks = **6 count**
4972
- **Total in chunk**: 70 count
73+
- Default: 100 spawners per chunk
74+
75+
### Per-Player Limit
76+
Limits total spawner stacks a player can place globally:
77+
- Tracks all spawners placed by each player across the entire server
78+
- Default: 1000 spawners per player
79+
- Can be increased via permission nodes (1500, 2000, 5000, etc.)
80+
- Bypass permission for unlimited spawners
81+
82+
**Both systems can be enabled/disabled independently in `config.yml`:**
83+
```yaml
84+
enable_chunk_limit: true # Enable chunk-based limiting
85+
enable_player_limit: true # Enable per-player limiting
86+
```
87+
88+
### Stack Counting Example
89+
- Player places 1 spawner with stack size 10 → **Adds 10** to both chunk and player count
90+
- Player breaks that spawner → **Removes 10** from both counts
91+
- Player stacks spawner from 10 to 20 → **Adds 10** more to both counts
92+
- Counts never go below 0 (zero-floor protection)
5093
5194
This ensures fair limiting based on actual spawner capacity, not just physical blocks.
5295
53-
## 🗄️ Database
96+
## Configuration
97+
98+
**config.yml:**
99+
```yaml
100+
# Chunk limit settings
101+
enable_chunk_limit: true
102+
max_spawners_per_chunk: 1000
103+
verify_chunk_count_on_check: true # Verify actual count from SmartSpawner API
104+
105+
# Per-player limit settings
106+
enable_player_limit: true
107+
max_spawners_per_player: 500 # Default limit for all players
108+
```
109+
110+
## Database
54111
55-
The plugin uses SQLite to store spawner chunk data. You can view and manage the database using:
112+
The plugin uses SQLite to store spawner data in two tables:
113+
- `spawner_chunks` - Chunk spawner counts
114+
- `player_spawners` - Per-player spawner counts
56115

57-
**[SQLite Viewer](https://sqliteviewer.app/)** - A free online tool to view and edit SQLite databases
116+
**[SQLite Viewer](https://sqliteviewer.app/)** - Free online tool to view and edit SQLite databases
58117

59118
Database location: `plugins/SSASpawnerLimiter/spawner_limits.db`
60119

61-
## 📄 License
120+
## License
62121

63122
This project is licensed under the CC-BY-NC-SA-4.0 License - see the [LICENSE](LICENSE) file for details.

src/main/java/github/io/ssaspawnerlimiter/SSASpawnerLimiter.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import github.io.ssaspawnerlimiter.database.DatabaseManager;
55
import github.io.ssaspawnerlimiter.listener.SpawnerLimitListener;
66
import github.io.ssaspawnerlimiter.service.ChunkLimitService;
7+
import github.io.ssaspawnerlimiter.service.PlayerLimitService;
78
import lombok.Getter;
89
import lombok.experimental.Accessors;
910
import org.bukkit.Bukkit;
@@ -31,6 +32,7 @@ public final class SSASpawnerLimiter extends JavaPlugin {
3132
private SmartSpawnerAPI api;
3233
private DatabaseManager databaseManager;
3334
private ChunkLimitService chunkLimitService;
35+
private PlayerLimitService playerLimitService;
3436
private BrigadierCommandManager commandManager;
3537
private Scheduler.Task cacheCleanupTask;
3638

@@ -39,9 +41,7 @@ private void checkSmartSpawnerAPI() {
3941
if (api == null) {
4042
getLogger().warning("SmartSpawner not found! This addon requires SmartSpawner to work.");
4143
getServer().getPluginManager().disablePlugin(this);
42-
return;
4344
}
44-
getLogger().info("SmartSpawner found, integrated with SSA Spawner Limiter");
4545
}
4646

4747
private void checkPluginUpdates() {
@@ -91,13 +91,17 @@ private void initializeServices() {
9191
// Initialize chunk limit service
9292
chunkLimitService = new ChunkLimitService(this, databaseManager);
9393

94+
// Initialize player limit service
95+
playerLimitService = new PlayerLimitService(this, databaseManager);
96+
9497
// Register event listeners
95-
Bukkit.getPluginManager().registerEvents(new SpawnerLimitListener(this, chunkLimitService), this);
98+
Bukkit.getPluginManager().registerEvents(new SpawnerLimitListener(this, chunkLimitService, playerLimitService), this);
9699

97100
// Start cache cleanup task (hardcoded: 5 minutes = 6000 ticks)
98101
long cleanupInterval = 6000L; // 5 minutes in ticks
99102
cacheCleanupTask = Scheduler.runTaskTimerAsync(() -> {
100103
chunkLimitService.cleanupExpiredCache();
104+
playerLimitService.cleanupExpiredCache();
101105
}, cleanupInterval, cleanupInterval);
102106
}
103107

src/main/java/github/io/ssaspawnerlimiter/command/MainCommand.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
44
import com.mojang.brigadier.tree.LiteralCommandNode;
55
import github.io.ssaspawnerlimiter.SSASpawnerLimiter;
6+
import github.io.ssaspawnerlimiter.command.subcommands.CheckPlayerSubCommand;
67
import github.io.ssaspawnerlimiter.command.subcommands.CheckSubCommand;
78
import github.io.ssaspawnerlimiter.command.subcommands.InfoSubCommand;
89
import github.io.ssaspawnerlimiter.command.subcommands.ReloadSubCommand;
@@ -33,6 +34,7 @@ public MainCommand(SSASpawnerLimiter plugin) {
3334
new ReloadSubCommand(plugin),
3435
new InfoSubCommand(plugin),
3536
new CheckSubCommand(plugin),
37+
new CheckPlayerSubCommand(plugin),
3638
new StatsSubCommand(plugin)
3739
);
3840
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package github.io.ssaspawnerlimiter.command.subcommands;
2+
3+
import com.mojang.brigadier.arguments.StringArgumentType;
4+
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
5+
import com.mojang.brigadier.context.CommandContext;
6+
import github.io.ssaspawnerlimiter.Scheduler;
7+
import github.io.ssaspawnerlimiter.SSASpawnerLimiter;
8+
import github.io.ssaspawnerlimiter.command.BaseSubCommand;
9+
import io.papermc.paper.command.brigadier.CommandSourceStack;
10+
import io.papermc.paper.command.brigadier.Commands;
11+
import org.bukkit.Bukkit;
12+
import org.bukkit.command.CommandSender;
13+
import org.bukkit.entity.Player;
14+
import org.jspecify.annotations.NullMarked;
15+
16+
import java.util.UUID;
17+
18+
@NullMarked
19+
public class CheckPlayerSubCommand extends BaseSubCommand {
20+
21+
public CheckPlayerSubCommand(SSASpawnerLimiter plugin) {
22+
super(plugin);
23+
}
24+
25+
@Override
26+
public String getName() {
27+
return "checkplayer";
28+
}
29+
30+
@Override
31+
public String getPermission() {
32+
return "ssaspawnerlimiter.command.checkplayer";
33+
}
34+
35+
@Override
36+
public String getDescription() {
37+
return "Check per-player spawner count";
38+
}
39+
40+
@Override
41+
public LiteralArgumentBuilder<CommandSourceStack> build() {
42+
LiteralArgumentBuilder<CommandSourceStack> builder = Commands.literal(getName());
43+
44+
builder.requires(source -> hasPermission(source.getSender()));
45+
46+
// Add player argument with suggestions
47+
builder.then(Commands.argument("player", StringArgumentType.word())
48+
.suggests((context, suggestionsBuilder) -> {
49+
for (Player p : Bukkit.getOnlinePlayers()) {
50+
suggestionsBuilder.suggest(p.getName());
51+
}
52+
return suggestionsBuilder.buildFuture();
53+
})
54+
.executes(this::executeWithPlayer));
55+
56+
// Add execute for when no player is provided (show usage)
57+
builder.executes(this::execute);
58+
59+
return builder;
60+
}
61+
62+
@Override
63+
public int execute(CommandContext<CommandSourceStack> context) {
64+
CommandSender sender = context.getSource().getSender();
65+
plugin.getMessageService().sendMessage(sender, "command_usage_checkplayer");
66+
return 0;
67+
}
68+
69+
private int executeWithPlayer(CommandContext<CommandSourceStack> context) {
70+
CommandSender sender = context.getSource().getSender();
71+
String playerName = context.getArgument("player", String.class);
72+
73+
Player target = Bukkit.getPlayer(playerName);
74+
if (target == null || !target.isOnline()) {
75+
plugin.getMessageService().sendMessage(sender, "command_checkplayer_player_not_found");
76+
return 0;
77+
}
78+
79+
UUID targetUUID = target.getUniqueId();
80+
81+
// Run async to avoid blocking
82+
Scheduler.runTaskAsync(() -> {
83+
int count = plugin.getPlayerLimitService().getPlayerSpawnerCount(targetUUID);
84+
int limit = plugin.getPlayerLimitService().getPlayerLimit(target);
85+
86+
// Send message on appropriate thread
87+
java.util.Map<String, String> placeholders = new java.util.HashMap<>();
88+
placeholders.put("player", target.getName());
89+
placeholders.put("current", String.valueOf(count));
90+
placeholders.put("limit", limit == Integer.MAX_VALUE ? "∞" : String.valueOf(limit));
91+
92+
if (sender instanceof Player player) {
93+
Scheduler.runAtLocation(player.getLocation(), () ->
94+
plugin.getMessageService().sendMessage(sender, "command_checkplayer_result", placeholders)
95+
);
96+
} else {
97+
plugin.getMessageService().sendMessage(sender, "command_checkplayer_result", placeholders);
98+
}
99+
});
100+
101+
return 1;
102+
}
103+
}
104+

src/main/java/github/io/ssaspawnerlimiter/command/subcommands/ReloadSubCommand.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,18 @@ public int execute(CommandContext<CommandSourceStack> context) {
4242
// Reinitialize language system
4343
plugin.getLanguageManager().reloadLanguages();
4444

45-
// Clear cache
45+
// Reload and clear cache for chunk limit service
4646
if (plugin.getChunkLimitService() != null) {
4747
plugin.getChunkLimitService().loadSpawnerLimit();
4848
plugin.getChunkLimitService().clearCache();
4949
}
5050

51+
// Reload and clear cache for player limit service
52+
if (plugin.getPlayerLimitService() != null) {
53+
plugin.getPlayerLimitService().loadConfiguration();
54+
plugin.getPlayerLimitService().clearCache();
55+
}
56+
5157
plugin.getMessageService().sendMessage(sender, "reload_success");
5258
return 1;
5359
} catch (Exception e) {

0 commit comments

Comments
 (0)