Skip to content

Commit d6db21d

Browse files
cryptobenchclaude
andcommitted
Add disk cache and smart tile refresh system
- Add DiskTileCache for persistent PNG storage across restarts - Tiles only refresh when players are nearby (terrain can change) - Add tileRefreshRadius and tileRefreshIntervalMs config options - Add /easywebmap pregenerate <radius> command to warm cache - Share TileManager across all connections (was creating new per request) - Update status command to show cache info - Increase default tileCacheSize to 20000 This dramatically reduces server load: - No cold start after restart (serve from disk) - Areas without players serve cached tiles instantly - Active areas refresh at most once per minute Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 721cf6c commit d6db21d

8 files changed

Lines changed: 459 additions & 36 deletions

File tree

README.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ ws.onmessage = (e) => {
7474

7575
| Command | What it does |
7676
|---------|--------------|
77-
| `/easywebmap status` | Show connection count and server info |
77+
| `/easywebmap status` | Show connection count, cache info, and server status |
7878
| `/easywebmap reload` | Reload the config file |
79-
| `/easywebmap clearcache` | Clear cached map tiles |
79+
| `/easywebmap clearcache` | Clear all caches (memory + disk) |
80+
| `/easywebmap pregenerate <radius>` | Pre-generate tiles around your position |
8081

8182
All commands require the `easywebmap.admin` permission.
8283

@@ -90,21 +91,84 @@ Config file: `mods/cryptobench_EasyWebMap/config.json`
9091
{
9192
"httpPort": 8080,
9293
"updateIntervalMs": 1000,
93-
"tileCacheSize": 500,
94+
"tileCacheSize": 20000,
9495
"enabledWorlds": [],
9596
"tileSize": 256,
9697
"maxZoom": 4,
97-
"renderExploredChunksOnly": true
98+
"renderExploredChunksOnly": true,
99+
"chunkIndexCacheMs": 30000,
100+
"useDiskCache": true,
101+
"tileRefreshRadius": 5,
102+
"tileRefreshIntervalMs": 60000
98103
}
99104
```
100105

101106
| Setting | Default | What it does |
102107
|---------|---------|--------------|
103108
| `httpPort` | 8080 | Web server port |
104109
| `updateIntervalMs` | 1000 | Player update frequency (ms) |
105-
| `tileCacheSize` | 500 | Max tiles to cache in memory |
110+
| `tileCacheSize` | 20000 | Max tiles to cache in memory (~200MB at 10KB/tile) |
106111
| `enabledWorlds` | [] | World whitelist (empty = all) |
107112
| `renderExploredChunksOnly` | true | Only render chunks that players have explored (prevents lag/abuse) |
113+
| `chunkIndexCacheMs` | 30000 | How long to cache the explored chunks index (ms) |
114+
| `useDiskCache` | true | Save tiles to disk for persistence across restarts |
115+
| `tileRefreshRadius` | 5 | Player must be within N chunks for tile to refresh |
116+
| `tileRefreshIntervalMs` | 60000 | Minimum time between tile refreshes (ms) |
117+
118+
### Chunk Index Cache (`chunkIndexCacheMs`)
119+
120+
When `renderExploredChunksOnly` is enabled, the plugin needs to check which chunks have been explored. This requires reading an index from disk. To avoid reading disk on every tile request, the index is cached.
121+
122+
**Trade-off:**
123+
- **Lower value** (e.g., 5000ms): New exploration shows on map faster, but more disk reads
124+
- **Higher value** (e.g., 60000ms): Fewer disk reads, but newly explored areas take longer to appear
125+
126+
**What this means in practice:**
127+
128+
| Cache Time | Disk Reads | Map Freshness |
129+
|------------|------------|---------------|
130+
| 5000 (5s) | ~12/min per world | New chunks visible within 5 seconds |
131+
| 30000 (30s) | ~2/min per world | New chunks visible within 30 seconds |
132+
| 60000 (1min) | ~1/min per world | New chunks visible within 1 minute |
133+
134+
**Example scenario:** A player explores a new area. With `chunkIndexCacheMs: 30000`, the new chunks won't appear on the web map until the cache expires (up to 30 seconds). The tile will show as empty until then.
135+
136+
**Note:** This only affects *newly* explored chunks. Already-explored chunks are always visible. The `/easywebmap clearcache` command clears this cache immediately if needed
137+
138+
### Disk Cache & Smart Refresh
139+
140+
The plugin uses a smart caching system to minimize server load:
141+
142+
1. **Disk Cache**: Tiles are saved as PNG files to `mods/cryptobench_EasyWebMap/tilecache/`. These persist across server restarts, so the first visitor after a restart doesn't trigger mass tile generation.
143+
144+
2. **Smart Refresh**: Tiles only regenerate when:
145+
- The tile is older than `tileRefreshIntervalMs` (default: 60 seconds), AND
146+
- A player is within `tileRefreshRadius` chunks (default: 5 chunks)
147+
148+
**Why this matters:**
149+
- If no players are nearby, terrain can't have changed, so the cached tile is always valid
150+
- This means 99% of tile requests serve instantly from cache with zero server load
151+
- Only actively played areas regenerate, and only once per minute at most
152+
153+
**Flow:**
154+
```
155+
Request for tile → Memory cache? → Serve instantly
156+
↓ no
157+
Disk cache? → Fresh enough? → Serve from disk
158+
↓ no ↓ old
159+
Generate new Players nearby? → No: Serve stale (terrain unchanged)
160+
↓ yes
161+
Regenerate tile
162+
```
163+
164+
### Pre-generation
165+
166+
Use `/easywebmap pregenerate <radius>` to warm the cache:
167+
- Generates tiles in a square around your position
168+
- Skips already-cached tiles and unexplored chunks
169+
- Runs in background with 50ms delay between tiles to avoid lag
170+
- Example: `/easywebmap pregenerate 50` generates up to 10,201 tiles
171+
- No max limit - use what you need (large values will take time)
108172

109173
---
110174

src/main/java/com/easywebmap/EasyWebMap.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.easywebmap.commands.EasyWebMapCommand;
44
import com.easywebmap.config.MapConfig;
5+
import com.easywebmap.map.TileManager;
56
import com.easywebmap.tracker.PlayerTracker;
67
import com.easywebmap.web.WebServer;
78
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
@@ -10,6 +11,7 @@
1011

1112
public class EasyWebMap extends JavaPlugin {
1213
private MapConfig config;
14+
private TileManager tileManager;
1315
private WebServer webServer;
1416
private PlayerTracker playerTracker;
1517

@@ -20,6 +22,7 @@ public EasyWebMap(JavaPluginInit init) {
2022
@Override
2123
public void setup() {
2224
this.config = new MapConfig(this.getDataDirectory());
25+
this.tileManager = new TileManager(this);
2326
this.webServer = new WebServer(this);
2427
this.playerTracker = new PlayerTracker(this);
2528
this.getCommandRegistry().registerCommand((AbstractCommand) new EasyWebMapCommand(this));
@@ -54,4 +57,8 @@ public WebServer getWebServer() {
5457
public PlayerTracker getPlayerTracker() {
5558
return this.playerTracker;
5659
}
60+
61+
public TileManager getTileManager() {
62+
return this.tileManager;
63+
}
5764
}

src/main/java/com/easywebmap/commands/EasyWebMapCommand.java

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,51 @@
1111
import com.hypixel.hytale.server.core.universe.PlayerRef;
1212
import com.hypixel.hytale.server.core.universe.world.World;
1313
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
14+
import com.hypixel.hytale.math.vector.Transform;
15+
import com.hypixel.hytale.math.vector.Vector3d;
1416
import java.awt.Color;
1517

1618
public class EasyWebMapCommand extends AbstractPlayerCommand {
1719
private static final Color GREEN = new Color(85, 255, 85);
1820
private static final Color YELLOW = new Color(255, 255, 85);
21+
private static final Color RED = new Color(255, 85, 85);
1922
private static final Color GRAY = new Color(170, 170, 170);
2023
private final EasyWebMap plugin;
2124
private final RequiredArg<String> subcommand;
2225

2326
public EasyWebMapCommand(EasyWebMap plugin) {
2427
super("easywebmap", "EasyWebMap admin commands");
2528
this.plugin = plugin;
26-
this.subcommand = this.withRequiredArg("action", "status|reload|clearcache", ArgTypes.STRING);
29+
this.subcommand = this.withRequiredArg("action", "status|reload|clearcache|pregenerate", ArgTypes.STRING);
2730
this.requirePermission("easywebmap.admin");
2831
}
2932

3033
@Override
3134
protected void execute(CommandContext ctx, Store<EntityStore> store, Ref<EntityStore> playerRef, PlayerRef playerData, World world) {
3235
String action = this.subcommand.get(ctx);
33-
switch (action.toLowerCase()) {
36+
String[] parts = action.split(" ");
37+
String cmd = parts[0].toLowerCase();
38+
39+
switch (cmd) {
3440
case "status" -> this.showStatus(playerData);
3541
case "reload" -> this.reloadConfig(playerData);
3642
case "clearcache" -> this.clearCache(playerData);
43+
case "pregenerate" -> this.pregenerate(playerData, world, parts);
3744
default -> this.showHelp(playerData);
3845
}
3946
}
4047

4148
private void showStatus(PlayerRef player) {
4249
int connections = this.plugin.getPlayerTracker().getConnectionCount();
4350
int port = this.plugin.getConfig().getHttpPort();
51+
int memoryCacheSize = this.plugin.getTileManager().getMemoryCacheSize();
52+
boolean diskCacheEnabled = this.plugin.getConfig().isUseDiskCache();
53+
4454
player.sendMessage(Message.raw("=== EasyWebMap Status ===").color(YELLOW));
4555
player.sendMessage(Message.raw("Web server: Running on port " + port).color(GREEN));
4656
player.sendMessage(Message.raw("WebSocket connections: " + connections).color(GREEN));
57+
player.sendMessage(Message.raw("Memory cache: " + memoryCacheSize + " tiles").color(GREEN));
58+
player.sendMessage(Message.raw("Disk cache: " + (diskCacheEnabled ? "Enabled" : "Disabled")).color(GREEN));
4759
player.sendMessage(Message.raw("URL: http://localhost:" + port).color(GREEN));
4860
}
4961

@@ -53,13 +65,51 @@ private void reloadConfig(PlayerRef player) {
5365
}
5466

5567
private void clearCache(PlayerRef player) {
56-
player.sendMessage(Message.raw("Tile cache cleared!").color(GREEN));
68+
this.plugin.getTileManager().clearCache();
69+
player.sendMessage(Message.raw("All caches cleared (memory + disk)!").color(GREEN));
70+
}
71+
72+
private void pregenerate(PlayerRef player, World world, String[] parts) {
73+
int radius = 10; // default
74+
if (parts.length > 1) {
75+
try {
76+
radius = Integer.parseInt(parts[1]);
77+
} catch (NumberFormatException e) {
78+
player.sendMessage(Message.raw("Invalid radius. Usage: /easywebmap pregenerate <radius>").color(RED));
79+
return;
80+
}
81+
}
82+
83+
if (radius < 1) {
84+
player.sendMessage(Message.raw("Radius must be at least 1.").color(RED));
85+
return;
86+
}
87+
88+
// Get player position as center
89+
Transform transform = player.getTransform();
90+
if (transform == null) {
91+
player.sendMessage(Message.raw("Could not get your position.").color(RED));
92+
return;
93+
}
94+
Vector3d pos = transform.getPosition();
95+
int centerX = (int) pos.x >> 4; // Convert to chunk coordinates
96+
int centerZ = (int) pos.z >> 4;
97+
98+
player.sendMessage(Message.raw("Starting pre-generation of " + ((radius * 2 + 1) * (radius * 2 + 1)) + " tiles...").color(YELLOW));
99+
player.sendMessage(Message.raw("This runs in the background. Check status with /easywebmap status").color(GRAY));
100+
101+
int finalRadius = radius;
102+
this.plugin.getTileManager().pregenerateTiles(world.getName(), centerX, centerZ, radius)
103+
.thenAccept(count -> {
104+
player.sendMessage(Message.raw("Pre-generation complete! Generated " + count + " new tiles.").color(GREEN));
105+
});
57106
}
58107

59108
private void showHelp(PlayerRef player) {
60109
player.sendMessage(Message.raw("=== EasyWebMap Commands ===").color(YELLOW));
61110
player.sendMessage(Message.raw("/easywebmap status - Show server status").color(GRAY));
62111
player.sendMessage(Message.raw("/easywebmap reload - Reload configuration").color(GRAY));
63-
player.sendMessage(Message.raw("/easywebmap clearcache - Clear tile cache").color(GRAY));
112+
player.sendMessage(Message.raw("/easywebmap clearcache - Clear all tile caches").color(GRAY));
113+
player.sendMessage(Message.raw("/easywebmap pregenerate <radius> - Pre-generate tiles around you").color(GRAY));
64114
}
65115
}

src/main/java/com/easywebmap/config/MapConfig.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ private void load() {
4545
this.data.renderExploredChunksOnly = defaults.renderExploredChunksOnly;
4646
needsSave = true;
4747
}
48+
if (!jsonObj.has("chunkIndexCacheMs")) {
49+
this.data.chunkIndexCacheMs = defaults.chunkIndexCacheMs;
50+
needsSave = true;
51+
}
52+
if (!jsonObj.has("tileRefreshRadius")) {
53+
this.data.tileRefreshRadius = defaults.tileRefreshRadius;
54+
needsSave = true;
55+
}
56+
if (!jsonObj.has("tileRefreshIntervalMs")) {
57+
this.data.tileRefreshIntervalMs = defaults.tileRefreshIntervalMs;
58+
needsSave = true;
59+
}
60+
if (!jsonObj.has("useDiskCache")) {
61+
this.data.useDiskCache = defaults.useDiskCache;
62+
needsSave = true;
63+
}
4864
}
4965
} catch (Exception e) {
5066
this.data = defaults;
@@ -107,13 +123,33 @@ public boolean isRenderExploredChunksOnly() {
107123
return this.data.renderExploredChunksOnly;
108124
}
109125

126+
public long getChunkIndexCacheMs() {
127+
return this.data.chunkIndexCacheMs;
128+
}
129+
130+
public int getTileRefreshRadius() {
131+
return this.data.tileRefreshRadius;
132+
}
133+
134+
public long getTileRefreshIntervalMs() {
135+
return this.data.tileRefreshIntervalMs;
136+
}
137+
138+
public boolean isUseDiskCache() {
139+
return this.data.useDiskCache;
140+
}
141+
110142
private static class ConfigData {
111143
int httpPort = 8080;
112144
int updateIntervalMs = 1000;
113-
int tileCacheSize = 500;
145+
int tileCacheSize = 20000;
114146
List<String> enabledWorlds = new ArrayList<>();
115147
int tileSize = 256;
116148
int maxZoom = 4;
117149
boolean renderExploredChunksOnly = true;
150+
long chunkIndexCacheMs = 30000;
151+
int tileRefreshRadius = 5;
152+
long tileRefreshIntervalMs = 60000;
153+
boolean useDiskCache = true;
118154
}
119155
}

0 commit comments

Comments
 (0)