Skip to content

Commit 0a499b9

Browse files
committed
#144 Update AGENTS overview
1 parent 65d1a94 commit 0a499b9

1 file changed

Lines changed: 33 additions & 16 deletions

File tree

AGENTS.md

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,19 @@ permission on PRs from forks by default in a public repo.
4646

4747
### Entry point and Minecraft hooks
4848

49-
`BlueMapSignMarkersMod` (`DedicatedServerModInitializer`) wires three lifecycle hooks:
50-
- `SERVER_STARTING``SignProvider.loadSigns(...)` reads the world's persisted `signs.json`
49+
`BlueMapSignMarkersMod` (`DedicatedServerModInitializer`) wires four lifecycle hooks:
50+
- `SERVER_STARTING``SignProvider.loadSigns(...)` reads the world's persisted, region-sharded sign storage
51+
(migrating a pre-sharding single `signs.json` on first boot after an upgrade)
5152
- `SERVER_STOPPING``SignProvider.saveSigns(...)` then `SignManager.stop()`
5253
- `BLOCK_ENTITY_LOAD` → for any loaded `SignBlockEntity`, calls `SignManager.addOrUpdate(...)`
54+
- `CHUNK_LOAD` → reconciles signs the mod's cache still knows about in a loading chunk against what's actually
55+
there; if a tracked sign's block is gone (e.g. its region file was deleted/regenerated externally while
56+
unloaded), calls `SignManager.remove(...)` for it. Addresses GitHub issue #110.
5357

54-
The markers file path is per-world: `config/bluemapsignmarkers/<world-save-name>/signs.json`.
58+
Sign state is stored per-world, region-sharded: one JSON file per (dimension, 32x32-chunk region) under
59+
`{server_root}/bluemapsignmarkers/{level}/{dimension_namespace}/{dimension_path}/r.{regionX}.{regionZ}.json`. A
60+
pre-sharding single `config/bluemapsignmarkers/<world-save-name>/signs.json` is migrated in place on first boot
61+
after upgrading (backed up, not deleted).
5562

5663
Two Mixins (`src/main/resources/bluemapsignmarkers.mixins.json`) catch the events the lifecycle hooks above can't:
5764
- `SignBlockEntityInject` injects into `SignBlockEntity.updateSignText` (a player edits a sign) → `SignManager.addOrUpdate`
@@ -62,13 +69,16 @@ Two Mixins (`src/main/resources/bluemapsignmarkers.mixins.json`) catch the event
6269

6370
1. **`SignHelper`** builds a `SignEntry` (immutable snapshot: position/dimension key, player id, parsed front/back
6471
text) from a `SignBlockEntity`, running sign text through a `SignLinesParser` configured with the current
65-
`MarkerGroup`s.
72+
`MarkerGroup`s. `SignHelper.reloadParser()` rebuilds that parser from the current config; it's called on every
73+
config reload (see below) so a sign parsed after `/bluemap reload` picks up an edited prefix/matchType.
6674
2. **`SignManager`** (singleton, holds a `ConcurrentMap<SignEntryKey, SignEntry>` cache of all known signs) is the
6775
decision point: given a new/updated `SignEntry`, it figures out whether this is an add, update, remove, or
6876
prefix-change (remove-then-add into a different marker group) relative to what's cached, then dispatches the
6977
corresponding `MarkerAction` (`AddMarkerAction`/`UpdateMarkerAction`/`RemoveMarkerAction`, built via
70-
`ActionFactory`) to the BlueMap connector. It also implements `IResetHandler.reset()` to replay the whole sign
71-
cache when BlueMap resets its state.
78+
`ActionFactory`) to the BlueMap connector. It also implements `IResetHandler.reset()`, which BlueMap fires on
79+
`/bluemap reload`: reloads config (`ConfigManager.reload()`, `SignHelper.reloadParser()`, rebuilding its
80+
prefix→group lookup and `ActionFactory`) then replays the whole sign cache through the same add/update/remove
81+
logic, so an edited marker-group's icon/offset/visibility/prefix takes effect without a server restart.
7282
3. **`BlueMapAPIConnector`** owns the `ReactiveQueue<MarkerAction>` and all actual BlueMap API calls. Because the
7383
BlueMap API is only available while BlueMap itself is enabled, actions are queued and only drained
7484
(`markerActionQueue.process()`) while `BlueMapAPI.getInstance().isPresent()`; `BlueMapAPI.onEnable`/`onDisable`
@@ -90,12 +100,17 @@ line to match the pattern (unlike `STARTS_WITH`, a regex prefix can't share its
90100

91101
### Sign persistence and versioning
92102

93-
Sign state is persisted per-world as `signs.json`, wrapped in a `VersionedSignFile` envelope (`{version, data}`) so
94-
the format can evolve without breaking old saves. `SignProvider.loadSigns` tries, in order: the versioned-file loader
95-
(`VersionedFileSignEntryLoader`, handling V2→V3 migration via `Version3Converter`, and current V3 files directly),
96-
then falls back to `Version1SignEntryLoader` for pre-versioning files. When adding a new persisted field, bump
103+
Sign state is stored per-world, region-sharded (one file per dimension + 32x32-chunk region — see "Entry point"
104+
above), with each region file wrapped in a `VersionedSignFile` envelope (`{version, data}`) so the format can evolve
105+
without breaking old saves. `SignProvider.loadSigns` checks whether the storage root already has region files
106+
(`RegionShardedSignEntryLoader.hasSignData`); if so, it loads every region file the same version-aware way as
107+
before sharding — the versioned-file loader (`VersionedFileSignEntryLoader`, handling V2→V3 migration via
108+
`Version3Converter`, and current V3 files directly), falling back to `Version1SignEntryLoader` for pre-versioning
109+
files. If no region files exist yet, `LegacySignFileMigrator` reads a pre-sharding single `signs.json` (if present)
110+
through that same version chain, writes it out region-sharded, and backs up the legacy file (renamed, not deleted)
111+
only once every expected region file is confirmed on disk. When adding a new persisted field, bump
97112
`SignFileVersions` and add a loader/converter rather than changing an existing version's shape in place — old
98-
`signs.json` files on live servers must keep loading.
113+
region files (or a not-yet-migrated legacy `signs.json`) on live servers must keep loading.
99114

100115
### Adding a new marker/BlueMap action
101116

@@ -107,15 +122,17 @@ compile.
107122
### Testable vs. game-coupled code
108123

109124
When adding logic, prefer keeping it in plain Java classes with no Minecraft/Fabric/BlueMap API types in their
110-
signature (like `SignLinesParser`, `SignEntryHelper`, `MarkerGroup`/`MarkerGroupMatchType`, `ConfigManager`/
111-
`ConfigProvider`, `ReactiveQueue`, the persistence loaders/converters, `ActionFactory`/`MarkerSetIdentifierCollection`)
125+
signature (like `SignLinesParser`/`ParsingContext`, `SignEntry`/`SignEntryHelper`, `SignChunkKey`/`SignChunkIndex`,
126+
`MarkerGroup`/`MarkerGroupMatchType`, `ConfigManager`/`ConfigProvider`, `ReactiveQueue`, `HtmlUtils`, `FileUtils`,
127+
the persistence loaders/converters (including `Version1SignEntryLoader`), `ActionFactory`/`MarkerSetIdentifierCollection`)
112128
— these can be unit tested directly (see `src/test/java/.../core/signs/SignLinesParserTest.java` for the pattern).
113129
Code that must reference game types (`SignHelper`, the mixins, `BlueMapSignMarkersMod`, `BlueMapAPIConnector`)
114130
should stay thin glue around the testable core, since it can only be verified manually via `runServer`.
115131

116-
There is a documented, currently-unfixed gap here: `BlueMapAPIConnector` passes sign text into BlueMap's POI marker
117-
`detail` field unescaped, and BlueMap renders `detail` as raw HTML (unlike `label`, which BlueMap escapes itself) —
118-
see `plans/html-detail-escaping-plan.md` for the planned fix.
132+
`BlueMapAPIConnector` escapes sign text (`HtmlUtils.toHtmlDetail`, in `common`) before it reaches BlueMap's POI
133+
marker `detail` field — BlueMap renders `detail` as raw HTML (unlike `label`, which BlueMap escapes itself), and
134+
sign text is player-controlled, so this closes a live XSS vector. See `plans/html-detail-escaping-plan.md` for the
135+
design. Persisted sign data stays raw/unescaped; escaping happens only at this BlueMap API call site.
119136

120137
## Planning documents
121138

0 commit comments

Comments
 (0)