@@ -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
5663Two 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
63701 . ** ` 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.
66742 . ** ` 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.
72823 . ** ` 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
109124When 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).
113129Code that must reference game types (` SignHelper ` , the mixins, ` BlueMapSignMarkersMod ` , ` BlueMapAPIConnector ` )
114130should 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