Compare commits

...

249 Commits

Author SHA1 Message Date
Antti Ellilä 0f472a818d
update finnish translation (#552) 2024-06-06 18:29:29 +02:00
Kyryl Andreiev bfec68e414
Update Ukraine and russian translations (#548)
* Update UA and RU translations

* Enchange UA translation

* Enhance UA transaltion
2024-06-06 17:24:56 +02:00
Lukas Rieger (Blue) 7895222816
Fix NPE when reading skull-owner uuidInts 2024-06-04 12:48:42 +02:00
Lukas Rieger (Blue) cca1fbc826
Fix exception when a player joins the server on fabric 2024-06-04 11:47:08 +02:00
Lukas Rieger (Blue) eeb01cba1a
Fix webserver not serving any map-assets (Fixes #549) 2024-06-04 11:31:20 +02:00
Lukas Rieger (Blue) 8f88dd7fd9
Another sql.php fix 2024-06-04 00:24:10 +02:00
Lukas Rieger (Blue) 33d40b888a
Fix sql.php problems 2024-06-04 00:14:34 +02:00
Lukas Rieger (Blue) 6392b67744
Remove unused chat-event 2024-06-03 22:48:03 +02:00
Lukas Rieger fbac48cd86
Merge pull request #537 from BlueMap-Minecraft/wip/v5
V5 (WIP)
2024-06-03 22:05:18 +02:00
Lukas Rieger (Blue) 4a38a7491b
Remove unneeded gradle wrappers 2024-06-03 21:59:27 +02:00
Lukas Rieger (Blue) 2dfd9feb21
Add implementations for the newest fabric/forge/neoforge versions 2024-06-03 21:50:52 +02:00
Lukas Rieger (Blue) 75b562eeb1
Generalize world-ids 2024-06-03 15:40:45 +02:00
Lukas Rieger (Blue) 474c5e27c4
Add fix-edges command and cli-option 2024-06-03 15:24:55 +02:00
Lukas Rieger (Blue) d43c7c474f
Apply spotless foxes and a comment 2024-06-03 12:26:37 +02:00
Lukas Rieger (Blue) 4b8245d19d
Fix webserver stalling 2024-06-03 10:52:14 +02:00
Lukas Rieger (Blue) b5e8bf42ae
Fix update-region expanding with force-updates & chunks with broken tile-entity data can now be loaded 2024-06-03 10:17:53 +02:00
Lukas Rieger (Blue) 7afcbeefd7
Fix addon loading issue 2024-05-28 21:56:53 +02:00
Lukas Rieger (Blue) 0cc0247930
Remove apache.commons.io and apache.commons.lang libraries 2024-05-28 21:54:14 +02:00
Lukas Rieger (Blue) e04e46fa5f
Spotless 2024-05-28 01:58:18 +02:00
Lukas Rieger (Blue) b1c75aa44a
Implement a Registry for BlockEntities 2024-05-28 01:57:35 +02:00
Lukas Rieger (Blue) 02d9fc1405
Use local variable 2024-05-27 23:48:49 +02:00
Lukas Rieger (Blue) 52d1e59108
Rename AddonManager to Addons 2024-05-27 23:46:32 +02:00
Lukas Rieger (Blue) 51185f5884
Apply spotless fixes 2024-05-27 23:11:02 +02:00
Lukas Rieger (Blue) 6ad50a89cb
Implement native addon loader 2024-05-27 23:10:20 +02:00
Lukas Rieger (Blue) 01b1ac513c
Merge branch 'wip/v5' of https://github.com/BlueMap-Minecraft/BlueMap into wip/v5 2024-05-26 16:37:07 +02:00
TechnicJelle fc8377764c
Show chunk borders (#542)
* Show chunk borders

* Change line width to width of two Minecraft pixels

* Also fade out chunkborders on hires tiles

The hires tiles just always had the chunkborders on them.
But the "fade out" distance of those models was 1000.
While the fade distance of the chunkborders on lowres tiles was between 200 and 600.
This would cause an uneven fadeout between the lowres tiles and the hires tiles.

* Added a toggle button for the chunk borders

* Move variable to better place
2024-05-26 16:33:11 +02:00
Lukas Rieger (Blue) a594a4bed3
comment & formatting 2024-05-22 20:58:02 +02:00
Lukas Rieger (Blue) 2c2d2f9227
Fix use of java-19 api 2024-05-22 16:03:58 +02:00
Lukas Rieger (Blue) 8455b50fc3
Fix use of implementation-specific exception 2024-05-22 16:00:54 +02:00
Lukas Rieger (Blue) 3db6833fc6
Move region-file watch service into World interface 2024-05-22 15:45:06 +02:00
Lukas Rieger (Blue) 20aa0a72f5
Apply spottless fixes 2024-05-21 22:24:31 +02:00
Lukas Rieger (Blue) 3faf2f0135
Only rebuild webapp if clean-build or there were changes 2024-05-21 22:23:57 +02:00
Lukas Rieger (Blue) d77d90c658
Fix some issues with the previous commit 2024-05-21 20:57:08 +02:00
Lukas Rieger (Blue) d7dd8931a5
Remove all usages of java.io.File and bad usages of Path.of() 2024-05-21 16:32:28 +02:00
Lukas Rieger (Blue) ce25eb52e3
Small improvements, make webserver more accessible for addons 2024-05-20 21:43:50 +02:00
Lukas Rieger (Blue) 93d8876b20
Rework StateDumper 2024-05-15 23:47:25 +02:00
Lukas Rieger (Blue) 3cd3f1d032
Switch to correct lz4 compression type 2024-05-15 01:20:46 +02:00
Lukas Rieger (Blue) 0b111463be
Apply spotless fixes 2024-05-15 00:46:37 +02:00
Lukas Rieger (Blue) b330c5d168
Improve readabillity 2024-05-15 00:45:34 +02:00
Lukas Rieger (Blue) 2029fe0a87
Add support for dimension-type directly stored in level.dat (#517) 2024-05-15 00:41:59 +02:00
Lukas Rieger (Blue) 2777846cf8
Fix max-height calculation (#535) 2024-05-15 00:01:54 +02:00
Lukas Rieger (Blue) 1b26803527
Update Caffeine, use soft values for chunk caches 2024-05-12 20:11:31 +02:00
Lukas Rieger 7cdc8213fa
Merge pull request #538 from Salzian/parallel-settings-loading
Parallelized initial settings loading
2024-05-09 12:40:51 +02:00
Salzian fe5c1fa785 Parallelized initial settings loading 2024-05-08 23:47:50 +02:00
Lukas Rieger (Blue) 909642d4c3
apply spotless fixes 2024-05-08 19:51:59 +02:00
Lukas Rieger (Blue) 05bbd2b481
Fix some more resource formatting 2024-05-08 19:37:40 +02:00
Lukas Rieger (Blue) 36c1d3f7ac
Restructure resource-extensions, fix some issues and add support for biome grass_color_modifier 2024-05-08 19:31:36 +02:00
TyBraniff a311fc1cef
Devolvement of issues #145 + missing signs (#536)
* Create cherry.json

* Create bamboo.json

* Create wall_cherry.json

* Create wall_bamboo.json

* Merging project

Merging TysFixes resource pack with main Bluemap

* Update blockColors.json

Merging TysFixes Phase3

* Merging projects

Adding everything from TysFixes Resource pack to core bluemap

* Update decorated_pot.json

* Update dragon_head.json

* Update dragon_wall_head.json
2024-05-07 16:48:22 +02:00
Lukas Rieger (Blue) 81fe41fd2b
Renderstate rewrite, and moving biomes to datapacks, mc-version and vanilla-resources and resource-extensions rewrite (wip) 2024-05-07 16:45:24 +02:00
Lukas Rieger (Blue) a6402850c9
Fix delete statements doing a full table scan 2024-04-18 15:46:45 +02:00
Lukas Rieger (Blue) 37dd18190b
Fix sqlite purgepurgeMapTilesStatement 2024-04-17 00:06:03 +02:00
Lukas Rieger (Blue) fa966c4363
Change popup-marker to always be in front of other markers 2024-04-08 13:37:34 +02:00
Lukas Rieger (Blue) 240ca6c00e
Fix some issues with the new sql-storage implementations 2024-04-07 12:55:41 +02:00
Lukas Rieger (Blue) f18f7a9a16
Update vite 2024-04-06 01:36:26 +02:00
Lukas Rieger (Blue) f66437ac83
Apply spotless fixes 2024-04-06 01:33:24 +02:00
Lukas Rieger (Blue) fdf242acdf
Rework storages to make them extensible with addons 2024-04-06 01:26:16 +02:00
Lukas Rieger (Blue) 7e7b1e4f53
Fix workflow name 2024-04-04 02:13:03 +02:00
Lukas Rieger (Blue) f097517320
Publish BlueMapCore and BlueMapCommon to BlueColored repo 2024-04-04 02:07:09 +02:00
Lukas Rieger (Blue) ee3ab6ff9a
Make use of updated spigot api to send command-messages in a better way 2024-03-29 13:39:12 +01:00
Lukas Rieger (Blue) 498a4f3190
Minimum required Java version is now 16, drop support for spigot versions < 1.16.5 2024-03-29 13:08:59 +01:00
Lukas Rieger (Blue) 757979b7b4
Implement equals and hashCode for BlueMapMap and BlueMapWorld 2024-03-24 00:07:10 +01:00
Lukas Rieger (Blue) 6e8247ae3a
Tentative fix for flickering with some custom animated textures 2024-03-21 14:40:20 +01:00
Gerber Lóránt Viktor a847e247e5
Add mechanism for retrieving BlockEntity data (#524)
* Add mechanism for retrieving BlockEntity data

This commit adds a mechanism for retrieving block entity data.
Block entity data is required to support for example text on signs,
banner patterns, or mods such as Domum Ornamentum.

* Fix the coordinate-packing for block entity-loading

This commit fixes the incorrect shifting of bits when
packing the chunk-local coordinates of a block entity
into a 64-bit long for lookups.

* Change mapping type of BlockEntity lookups

This commit changes the type stored for BlockEntity
mappings from a class of the type associated with the
ID to a method reference to its constructor.

* Tidy BlockEntity mappings

This commit introduces a small functional interface
to make the type less ungodly. Also silences the warning
about referencing subclasses in the superclass, it is
fine in this case, we're just storing a reference to
the constructor.

* Add missing license headers

The license headers were missing. Oops.
2024-03-20 23:23:03 +01:00
Lukas Rieger (Blue) 2689cd10e0
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2024-03-20 22:05:51 +01:00
Lukas Rieger (Blue) b60b14372f
Fix menu-title of player-markers not translatable 2024-03-20 22:05:38 +01:00
Gerber Lóránt Viktor 10fb88df4b
Make TextureVariable references align with game behaviour (#525)
While the current implementation of reference handling in
this class was the correct way to go (only handling
texture names starting with # as references), the game is
happy to accept references without a leading hashtag, since
it just chops it off and continues on the same code path
regardless.

This commit makes the reference handling in BlueMap align
with this behaviour, potentially allowing "broken" models
to render as they do in game.

This method works for reference resolving, since if a string
passed into the texture field contains a ':' then it must be
a namespaced key, and if it contains a '/' it has to be a
resource key, because the 'minecraft' namespace is implied
in these cases. The other way around, if someone were to pass
in a string like 'oak_planks', it is safe to assume it is a
reference, since the implied resource key would be
'minecraft:oak_planks', but textures aren't at the root level
in that namespace.
2024-03-17 14:56:32 +01:00
Lukas Rieger (Blue) 9fca7b9361
preserve lf lineseparator for config templates 2024-03-17 13:15:07 +01:00
Lukas Rieger (Blue) a0e9180360
Fix paper-implementation not saving the world on the main thread 2024-03-14 09:17:22 +01:00
Lukas Rieger (Blue) e9e7042aed
Fix spotless testing for root-modules 2024-03-04 13:10:00 +01:00
Lukas Rieger (Blue) b27aedc4c2
Fix spotless error 2024-03-04 12:57:49 +01:00
Lukas Rieger (Blue) 0613037093
Fix paper-implementation not detecting additional worlds correctly 2024-03-04 12:50:03 +01:00
Lukas Rieger (Blue) aecbd23ba7
Implement animated textures 2024-02-26 01:59:28 +01:00
Lukas Rieger (Blue) 2899646adc
Swap chunk-scan direction for future stuff :) 2024-02-25 18:12:27 +01:00
Lukas Rieger (Blue) c9a8c83d6e
Fix map hires sometimes not loading when loading a map 2024-02-25 15:09:10 +01:00
Lukas Rieger (Blue) ceb31b68eb
Add more cache-control headers to sql.php 2024-02-25 02:19:40 +01:00
Lukas Rieger (Blue) b625af695c
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2024-02-25 02:15:45 +01:00
Lukas Rieger (Blue) 908789a815
Add more cache-control headers to integrated webserver 2024-02-25 02:15:34 +01:00
Nikita c0c946d154
Update ru translation (#515)
Co-authored-by: NikitaCartes-forks <66517597+NikitaCartes-forks@users.noreply.github.com>
2024-02-25 01:26:08 +01:00
Lukas Rieger (Blue) b02b91d3bb
Use the global cache-hash for settings and textures .json requests 2024-02-25 01:00:20 +01:00
Antti Ellilä b437684dbb
Workflow to check translations for outdated data and missing strings (#514)
* Workflow to check translations for outdated data and missing strings

* Run the workflow when checker changes too
2024-02-24 23:58:21 +01:00
Lukas Rieger (Blue) 5bb7a77fb9
Actually fix NPE when loading chunks sometimes 2024-02-24 23:51:23 +01:00
Lukas Rieger (Blue) 0d36a0f70b
Fix region file watcher ignoring linear files 2024-02-24 22:09:25 +01:00
Lukas Rieger (Blue) 35c236e9ce
Turn storage-enum into a registry 2024-02-24 14:11:56 +01:00
Lukas Rieger (Blue) 79ea7baba7
Fix biomes-packed array sometimes sized incorrectly 2024-02-24 13:37:02 +01:00
Lukas Rieger (Blue) 122ba83ebb
Correct content-type header and response for empty map tiles 2024-02-24 10:44:25 +01:00
Lukas Rieger (Blue) d1aba560da
Fix possible self-supression 2024-02-23 23:55:17 +01:00
Lukas Rieger (Blue) 9e8dc8e5a8
Tentative fix for heightmap data being null 2024-02-23 23:55:01 +01:00
Lukas Rieger (Blue) ec101feb94
Add relocation for bluenbt 2024-02-23 21:58:50 +01:00
Lukas Rieger (Blue) 97c6640721
Change metrics implementation key for paper and spigot 2024-02-23 21:52:41 +01:00
Lukas Rieger (Blue) 2c341fc894
Apply spotless fixes 2024-02-23 21:51:29 +01:00
Lukas Rieger (Blue) 40119127ee
Pack normals into one byte instead of 4 to save space :) 2024-02-23 18:02:29 +01:00
Lukas Rieger (Blue) 6e68a8f0e0
[breaking] Switch hires tile format to prbm (modified prwm) 2024-02-23 17:32:07 +01:00
Lukas Rieger (Blue) 3a1e723a51
Improve linear region efficiency by caching the whole region-file data 2024-02-23 00:55:11 +01:00
Lukas Rieger (Blue) dbde93c9f5
Reimplement Linear region file format support 2024-02-22 23:23:56 +01:00
Lukas Rieger (Blue) ff1e38a7e1
Fix Map-Updates not working correctly 2024-02-22 12:58:57 +01:00
TechnicJelle 2dd7a0a9c2
Log user-added Scrips & Styles when they load in (#506) 2024-02-14 21:23:27 +01:00
Lukas Rieger (Blue) cc50e05262
Use floats for model-positions instead of doubles 2024-02-09 15:30:23 +01:00
Lukas Rieger (Blue) 73a77e5e0e
Do light/cave testing before face-culling to improve performance 2024-02-08 15:26:45 +01:00
Lukas Rieger (Blue) 81e8da3b70
Fix storage performance 2024-02-08 13:18:49 +01:00
Lukas Rieger (Blue) e02a43a521
Tidy up buildscript and dependencies, fix spongeworld world-folder being wrong 2024-02-07 23:01:31 +01:00
Lukas Rieger (Blue) 74c68c3428
Drop support for fabric 1.15-1.17 and forge 1.17 2024-02-07 20:51:09 +01:00
Lukas Rieger (Blue) 584883444a
Push BlueMapAPI 2024-02-07 20:50:21 +01:00
Lukas Rieger 16981f2797
Refactor World-Management and Region/Chunk-Loading (#496)
* Implement PackedIntArrayAccess

* First working render with BlueNBT

* Progress converting chunkloaders

* Core rewrite done

* WIP - Restructuring configs and world-map mapping

* WIP - Compiling and starting without exceptions :)

* Fix cave detection

* Ensure configuration backwards compatibility (resolve dimension from configured world if missing)

* Implement support for 1.16+ chunks

* Implement support for 1.15+ chunks

* Implement support for 1.13+ chunks and some fixes

* Also find worlds based on their id again in BlueMapAPI

* Improve autogenerated config names

* Implement equals for all ServerWorld implementations

* Get rid of var usage
2024-02-07 20:43:37 +01:00
Lukas Rieger (Blue) efd45658d5
Fix empty blockstates having the wrong block-properties 2024-02-03 00:59:41 +01:00
Lukas Rieger (Blue) b3c4f3737d
Webapp: Fix screen-pos calculation on click 2024-01-25 15:28:33 +01:00
Lukas Rieger (Blue) 61f883b134
Add -m option to cli, which allows to filter the maps that should be loaded and rendered 2024-01-23 19:35:25 +01:00
Lukas Rieger (Blue) 6a10fac624
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2024-01-22 09:52:07 +01:00
Lukas Rieger (Blue) 039a95c813
Update vite 2024-01-22 09:52:02 +01:00
jhqwqmc b1aba76d09
Update zh_CN.conf (#501) 2024-01-18 13:43:33 +01:00
Lukas Rieger (Blue) a703a92357
Remove pretty-printing from settings.json 2024-01-17 18:27:44 +01:00
Lukas Rieger (Blue) 50b7f3670e
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2024-01-14 10:05:05 +01:00
Lukas Rieger (Blue) d6e4e69417
Fix unlisted markers being counted 2024-01-14 10:05:00 +01:00
Arkyoh 6bf4291779
Update fr.conf (#497) 2024-01-06 10:41:47 +01:00
Lukas Rieger (Blue) 6ce32c56dc
Fix loading texture self-healing not working if its a json-syntax error 2024-01-04 17:19:34 +01:00
birbkeks e62919f14f
updated Turkish language (#495)
* Update tr.conf

fixed typos

* Update tr.conf

fixed more typos
2023-12-23 20:14:01 +01:00
TechnicJelle 7c3485363e
Fix grammar in BlueMapService.java (#494) 2023-12-14 16:26:31 +01:00
Lukas Rieger (Blue) a0b47f1bd5
Fix texture-gallery not preserving textures that are missing after a resource(pack) change 2023-12-11 12:35:12 +01:00
Lukas Rieger (Blue) ef728dee06
Declare 1.20.4 support where applicable 2023-12-10 13:17:15 +01:00
Lukas Rieger (Blue) 72b71a91e8
Add missing version on paper modrinth publish 2023-12-06 20:35:45 +01:00
Lukas Rieger (Blue) 8104f99f60
Declare 1.20.3 support in publication where applicable 2023-12-06 19:57:41 +01:00
Lukas Rieger (Blue) 78904b4051
Add resource-patch to make 1.20.3 resources more backwardscompatible with 1.20.2 2023-12-06 19:24:07 +01:00
Lukas Rieger (Blue) 6307fb1e6b
Add 1.20.3 resources 2023-12-06 17:25:30 +01:00
Lukas Rieger (Blue) c7f340dec3
Add neoforge implementation 2023-12-06 17:19:18 +01:00
Lukas Rieger (Blue) 3f82a821a2
Fix keysize issue once more 2023-11-23 21:58:59 +01:00
Lukas Rieger (Blue) 36090e8545
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-11-21 16:16:01 +01:00
Lukas Rieger (Blue) 5866cb5766
Add basic support for the resource-changes of the 1.20.3 snapshots 2023-11-21 16:15:54 +01:00
Albus Rex 0bad1b6b2a
PDO php script implementation (#486)
* pdo connector implementation

* renamed php script

* made mysql default

* made errors generic

* Some small changes to make it work with mysql

* Better newlines

---------

Co-authored-by: AlbusRex <braisskarget@gmail.com>
Co-authored-by: Lukas Rieger (Blue) <TBlueF@users.noreply.github.com>
2023-11-20 12:40:47 +01:00
Lukas Rieger (Blue) 5b93202994
Fix project-id for sponge publication 2023-11-19 18:18:49 +01:00
Lukas Rieger (Blue) 20d32cb9c9
Hide map-menu button if there is only one map, hide marker menu button if there are no markers 2023-11-19 17:15:08 +01:00
Lukas Rieger (Blue) 6f015da070
Fix project-building from scratch 2023-11-19 16:13:35 +01:00
Lukas Rieger (Blue) deafe50305
Add releasenotes to publications 2023-11-19 16:00:46 +01:00
Lukas Rieger (Blue) 73103eda3b
Improve redraw-trigger on settings-changes 2023-11-19 13:42:56 +01:00
Lukas Rieger (Blue) 77c1e42009
Change default nether and end configs to have a better void color 2023-11-19 11:22:43 +01:00
Lukas Rieger (Blue) 5f0942a8ae
Fix error when purging a map but the directory is already deleted. Fixes: #490 2023-11-18 16:32:05 +01:00
Lukas Rieger (Blue) 28c9166030
Define a safe collation for mysql tables. Fixes: #488 2023-11-18 16:26:56 +01:00
Lukas Rieger (Blue) c7af5e9639
Fix url angle in free-flight not applying correctly. Fixes: #485 2023-11-18 14:45:06 +01:00
Lukas Rieger (Blue) 6d3774f24e
Set fallback void color to black, pause high fps redraws if nothing changes to save GPU usage 2023-11-18 14:20:45 +01:00
Lukas Rieger c69b31ce84
Fix invalid usage of String.format on paper implementation 2023-11-16 23:15:07 +01:00
Lukas Rieger 665af5583f
Fix BlockProperties being stacked the wrong way around 2023-11-14 21:42:46 +01:00
Lukas Rieger (Blue) 24a6b2f370
Fix freeflight option not removed from menu. Fixes: #482 2023-10-06 08:26:10 +02:00
Nikita 609d6a90db
Migrate fabric implementation to events system (#481) 2023-10-03 12:31:45 +02:00
Lukas Rieger (Blue) 1ae6fc790d
Add gradle-deployment to Ore 2023-10-02 16:58:22 +02:00
Lukas Rieger (Blue) 5ac293ca18
Only support latest recommended sponge version and fix default config generation on sponge 2023-10-02 16:33:08 +02:00
TechnicJelle 48445db7b5
Add map config for having a different void colour (#477)
* Added global webapp option for not having a void

* Fix for non-default lighting conditions

* Replace `isVoid` with whole `voidColor` feature!

* Default void colour should be black

* And now the default sky colour can also be set back to what it was

* Fix the low-res void colour

* Add config option for the new void colour setting!

(I hope I haven't forgotten any place to add it, but it does work, so I don't think so..?)
2023-09-30 23:42:37 +02:00
Lukas Rieger (Blue) e087f17564
Fix hangar publication 2023-09-28 14:07:11 +02:00
Lukas Rieger (Blue) 808885b66a
Push BlueMapAPI 2023-09-27 23:51:01 +02:00
Lukas Rieger (Blue) a665fb377d
Merge branch 'fix/gh-action' 2023-09-27 23:49:27 +02:00
Lukas Rieger (Blue) 15329b4f5d
Add option to manually trigger gh-action 2023-09-27 23:16:38 +02:00
Lukas Rieger (Blue) eb258e7c33
Testing stuff..3 2023-09-27 23:14:46 +02:00
Lukas Rieger (Blue) 4d38a2953d
Testing stuff..2 2023-09-27 23:11:23 +02:00
Lukas Rieger (Blue) 71fcd91c3e
Testing stuff.. 2023-09-27 23:09:53 +02:00
Lukas Rieger 01027af969
Update Gradle and 1.20.2 support (#479)
* Update gradle

* Update Fabric-Loom and ForgeGradle

* Add fabric-1.20.2 implementation

* Finalize 1.20.2 update

---------

Co-authored-by: NikitaCartes <nikich98@yandex.ru>
2023-09-27 17:39:47 +02:00
TechnicJelle 7047df73d0
Made webserver binding logging less confusing (#474)
People often think "0.0.0.0" is the IP they should connect to when BlueMap logs `WebServer bound to: /0.0.0.0:8100`
Or they see the IPv6 address being logged, and think BlueMap is only running on IPv6.
2023-09-07 08:41:50 +02:00
Lukas Rieger (Blue) c407ba6bd5
Fix supported versions 2023-09-05 15:37:59 +02:00
Lukas Rieger (Blue) e1701c4754
Add paper support to folia implementation 2023-09-05 15:34:46 +02:00
Lukas Rieger (Blue) 4663eb715b
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-09-05 14:22:36 +02:00
Lukas Rieger (Blue) 7156993323
Make webserver resource-closing more watertight against potential leaks 2023-09-05 14:22:30 +02:00
YuRaNnNzZZ aff64294af
Fix tall non-animated textures getting cut off (#472) 2023-08-23 21:55:51 +02:00
Lukas Rieger (Blue) 155f56e62a
Push BlueMapAPI 2023-08-20 08:25:28 +02:00
Lukas Rieger (Blue) 15d402cceb
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-08-10 16:10:06 +02:00
Lukas Rieger (Blue) 4aadaeb2e0
Fix indentation of frozen status on map list 2023-08-10 16:09:57 +02:00
Lukas Rieger (Blue) 4386e35c59
Dont delete the file since we are replacing it anyways, to minimize the risk of deleting the file without a replacement 2023-08-10 16:08:48 +02:00
TWME 87032fde81
Update zh_TW.conf (#460)
* Update zh_TW.conf

* Create zh_TW_new.conf

* Update zh_TW.conf

* Delete zh_TW_new.conf

* Update zh_TW.conf

* Update zh_TW.conf

* Update zh_TW.conf

* Update zh_TW.conf

* Update zh_TW.conf

* Update zh_TW.conf

* Update zh_TW.conf
2023-07-30 16:17:15 +02:00
Lukas Rieger (Blue) 9de49dc313
Add paper and purpur to modrinth publish 2023-07-27 22:08:52 +02:00
Lukas Rieger (Blue) 0eb12a1588
Fix webapp error when player-markers are disabled 2023-07-16 02:03:54 +02:00
Lukas Rieger (Blue) eda3815b89
Fix publish versions 2023-07-15 13:45:44 +02:00
Lukas Rieger (Blue) bc2327ec74
Bump BlueMapAPI 2023-07-15 12:59:39 +02:00
Lukas Rieger (Blue) 5028161b66
Correct compression naming for deflate compression 2023-07-13 12:27:34 +02:00
Lukas Rieger (Blue) c851812f42
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-07-11 20:07:24 +02:00
Lukas Rieger (Blue) 074bc4a436
Fix not able to teleport to players on other worlds. Fixes: #451 2023-07-11 20:07:10 +02:00
TechnicJelle eaac9f8f84
Translated new strings to Dutch (#453) 2023-07-09 15:33:32 +02:00
Lukas Rieger (Blue) 282b3806f3
Do not reset camera when pressing the update-map button 2023-07-09 15:25:38 +02:00
Lukas Rieger (Blue) 83b81bcca6
Add support for c2me-uncompressed chunks 2023-07-07 13:15:17 +02:00
Lukas Rieger (Blue) 838b22aa19
Add zlib and zstd to the usable compression formats
(They are available in the dependencies anyways so why not add them here ^^)
2023-07-05 11:02:22 +02:00
Lukas Rieger (Blue) 1f7f51c1e1
Add webapp option to default to flat-view 2023-07-04 14:42:12 +02:00
Lukas Rieger (Blue) 16c4b281ef
Fix settings.json and markers.json not being written with the correct charset 2023-07-03 13:03:52 +02:00
Lukas Rieger (Blue) 1b2dc45b4b
Add debug-log, improve logging in general and fix weblogger not closing correctly 2023-07-01 09:44:19 +02:00
Lukas Rieger (Blue) 63ece941ad
Update GitHub action cache version for a fix with cache sizes greater than 2GB 2023-06-29 14:09:53 +02:00
Lukas Rieger (Blue) 15f5e8fd00
Fix default log file name 2023-06-29 13:51:41 +02:00
Lukas Rieger (Blue) 0ebea9982f
Apply Spotless fixes for BlueMapCommon 2023-06-29 13:42:56 +02:00
Lukas Rieger (Blue) 7c56fc49a7
Apply Spotless fixes for BlueMapCore 2023-06-29 13:42:36 +02:00
Lukas Rieger (Blue) d570884def
Add advanced webserver logging + settings 2023-06-29 13:37:31 +02:00
Lukas Rieger (Blue) 8f97b08eb5
Use string-builder in verbose logger 2023-06-29 08:38:46 +02:00
Lukas Rieger (Blue) 4141d21a70
Add support for xff header in verbose web logger 2023-06-29 08:29:53 +02:00
Lukas Rieger (Blue) 1b26811c6a
Bump BlueMapAPI 2023-06-26 18:56:02 +02:00
Lukas Rieger (Blue) 0ac939a644
Always initialize PlayerSkinUpdater to be available to API 2023-06-22 08:21:36 +02:00
Lukas Rieger (Blue) 72264a2e86
Add supressed exception for more info 2 2023-06-21 15:13:27 +02:00
Lukas Rieger (Blue) e66838cdbc
Add supressed exception for more info 2023-06-21 15:11:48 +02:00
Lukas Rieger (Blue) 987015b617
Fix wrong config-key in plugin.conf template 2023-06-21 09:05:53 +02:00
Lukas Rieger (Blue) fdd4713dfb
Fix command permission for deleting a map from a storage 2023-06-20 22:05:24 +02:00
Lukas Rieger (Blue) b4b3e72d51
Make configs generate paths with forwardslashes whenever possible 2023-06-19 15:44:57 +02:00
Lukas Rieger (Blue) 31ae055ae5
Fix marker-set default-hide not working anymore 2023-06-19 11:47:53 +02:00
Lukas Rieger (Blue) 2f78f75a90
escape marker-set id when saving to local storage 2023-06-19 11:42:25 +02:00
Lukas Rieger (Blue) 76e3c4e758
Fix markers being selectable 2023-06-19 11:28:11 +02:00
Lukas Rieger (Blue) 49f76c1b11
Fix markers overlap over menu if more than 100 markers are on the map 2023-06-19 11:05:56 +02:00
Lukas Rieger (Blue) b3c7d89793
revert accidentally committed vite config change 2023-06-18 23:52:41 +02:00
Lukas Rieger (Blue) 55095f1b5e
Add capabillity to use -follow players- in first-person mode. Closes: #175 2023-06-18 23:44:07 +02:00
Lukas Rieger (Blue) cb638910ce
Remember hide/show status of marker-sets 2023-06-18 17:15:44 +02:00
Lukas Rieger (Blue) 85f4735050
Fix hours in AM/PM format instead of 24 2023-06-18 13:51:07 +02:00
Lukas Rieger (Blue) 2e572ddb11
Fix status command formatting and add last render times to status and maps commands 2023-06-18 13:46:41 +02:00
Lukas Rieger (Blue) 7097547301
Add sqlite support. Closes: #322 2023-06-18 01:02:13 +02:00
Lukas Rieger (Blue) 55fb955ed7
Fix clear() method not actually clearing the markerset. Fixes: #430 2023-06-17 13:33:42 +02:00
Lukas Rieger (Blue) e46efc4c53
Use root-map element for scroll-events. Fixes: #409 2023-06-17 12:42:34 +02:00
Lukas Rieger (Blue) 22f2b09fe5
Fix Mobile first-person Controls not showing. Fixes: #447 2023-06-17 11:59:16 +02:00
Lukas Rieger (Blue) 4fc6d7f889
Fix stuck moving when window looses focus. Fixes: #408 2023-06-17 11:53:39 +02:00
Lukas Rieger (Blue) 0f7fd4ccd4
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-06-16 20:23:42 +02:00
Lukas Rieger (Blue) 97f346534b
Fix 1.13.2 support 2023-06-16 20:23:28 +02:00
Antti Ellilä da8a12158b
Expose more JavaScript classes (#449)
With the lack of a proper JavaScript API for addons, it might be useful to just expose all the classes in the global namespace for usage and code injection.
2023-06-15 20:08:36 +02:00
stdpi bac87ec546
chore: add Vietnamese translation (#448) 2023-06-15 19:12:22 +02:00
Lukas Rieger (Blue) 969f7a78f3
Update CONTRIBUTING.md 2023-06-14 19:27:22 +02:00
Lukas Rieger (Blue) d120a00496
rename _index.php to mysql.php 2023-06-12 23:14:05 +02:00
Lukas Rieger (Blue) 82f1e1321d
Rename Dialect interface 2023-06-12 23:09:25 +02:00
Lukas Rieger (Blue) 32f15d6555
Use fallback dialect instead of returning null 2023-06-12 23:07:00 +02:00
MrSolarius f149b823a7
Add support for postgres databases (#443)
* Refactor : wrap every single SQL query inside an interface

* Feat : create every SQL request for postgres

* Refactor : rename SQLQueryAbstractFactory to SQLQueryFactory

* Feat : add dialect settings to blue map !

* Feat : Create two new storageClass for different storage approche

* Feat : add read BYTEA support

* Fix : remove unuseful println

* Fix : remove edited sql.conf

* Refactor / Feat : support for mysql

* Lots of tiny tweaks

---------

Co-authored-by: Lukas Rieger (Blue) <TBlueF@users.noreply.github.com>
2023-06-12 22:55:44 +02:00
TechnicJelle c807699c7c
Add port-in-use check (#440)
* Add port-in-use check to plugin

* Add port-in-use check to CLI
2023-06-08 23:12:20 +02:00
Lukas Rieger (Blue) f79254115e
Do spotless checks on each github action build 2023-06-08 22:15:21 +02:00
Lukas Rieger (Blue) 924734b72d
Apply spotless fixes 2023-06-08 22:01:31 +02:00
Lukas Rieger (Blue) 92fc2a582b
Add forge-1.20 implementation 2023-06-08 18:22:21 +02:00
Lukas Rieger 16b1300ced
Minecraft 1.20 (#438)
* Add 1.20 resource link

* Add fabric 1.20 implementation

* Fix publish versions

* 1.20-pre7 (#437)

* Fix chunk status now having a namespace .. yay:)

* Update vite for security-issue fix
(We are not affected, but updating doesnt hurt)

* Remove all but latest 1.19 version

* Final 1.20 updates

---------

Co-authored-by: Aurélien <43724816+Aurelien30000@users.noreply.github.com>
2023-06-07 17:33:27 +02:00
Lukas Rieger (Blue) 4cbad657d3
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-06-07 14:18:53 +02:00
Lukas Rieger (Blue) 22917d36eb
Update vite for security-issue fix
(We are not affected, but updating doesnt hurt)
2023-06-07 14:18:46 +02:00
Carey Metcalfe 1eb9982357
Allow the build to work in cases where the current repo has no tags (#434)
When forking the project on GitHub there is an option to only fork the
main repo branch. This means that the forked repo will not have any tags
in it which currently causes the build system to fail.

This change adds a fallback in the case that there are no tags to
building a version named "-dev-dirty".
2023-05-19 12:14:31 +02:00
Lukas Rieger (Blue) e2037e9698
Run status-command in separate thread 2023-05-15 17:40:47 +02:00
Lukas Rieger (Blue) 083c6c06c2
Improve progressEstimation function 2023-05-15 17:18:25 +02:00
Lukas Rieger (Blue) aed400ddae
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-05-15 17:01:50 +02:00
Antti Ellilä 3b7ee64d37
feat: arm64 support for docker image (#432) 2023-05-15 17:01:44 +02:00
Lukas Rieger (Blue) 742f96db6a
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-05-15 17:01:28 +02:00
Lukas Rieger (Blue) f1f2336dff
Remove syncronization from estimateProgress() function 2023-05-15 17:01:10 +02:00
z-glitch 463f5cb120
Update URL for 3rd party addons and tools. (#431) 2023-05-14 11:55:35 +02:00
Lukas Rieger (Blue) 659fb99eb6
Avoid NPE if accessing the API while bluemap is unloaded 2023-05-14 02:18:31 +02:00
Lukas Rieger (Blue) 5933d43b62
Publish folia version to modrinth 2023-05-13 11:33:41 +02:00
Lukas Rieger (Blue) a31785e67e
Swap zstd library and relocate it correctly 2023-05-09 22:52:29 +02:00
Lukas Rieger (Blue) 92e5300c1e
Clear chunks from cache when starting a render-task for them 2023-05-09 22:09:18 +02:00
Lukas Rieger (Blue) 8bac85ce22
Merge branch 'master' of https://github.com/BlueMap-Minecraft/BlueMap 2023-05-08 16:16:01 +02:00
Lukas Rieger (Blue) c5c791bd0d
Tentative fix for Folia Player-Update ask scheduling error 2023-05-08 16:15:48 +02:00
Lukas Rieger (Blue) 892d3e297e
Push BlueMapAPI 2023-05-08 16:15:00 +02:00
Antti Ellilä f467220400
Start rendering after everything else is loaded (#425)
To attempt to fix the weirdness with tile filters not working at startup
2023-05-07 19:24:43 +02:00
Sofiane H. Djerbi b9dbb100e4
Linear timestamps (#424)
* Preparing for Linear v2

* Support for linear v2
2023-05-07 09:36:01 +02:00
Lukas Rieger (Blue) 94dbd372dc
Fix typo 2023-05-01 20:43:16 +02:00
Lukas Rieger (Blue) 004f296b5e
Add storages command 2023-05-01 20:41:30 +02:00
Antti Ellilä 0fce08dd62
Add version flag for cli (#414)
* Add version flag for cli, rename mc-version flag

* Change to -V, revert mc-version
2023-04-30 11:10:42 +02:00
Lukas Rieger f2355fa99b
feat: Linear region file format support (extended from #415) (#418)
* Linear support

Fix region rendering & bitwise operators

Close streams

* Make mca region-file types extensible

* Fix file-name verification not working

---------

Co-authored-by: Sofiane H. Djerbi <46628754+kugge@users.noreply.github.com>
2023-04-30 11:09:36 +02:00
MidnightTale bc9e688317
Thai translation (#412)
* Update settings.conf

* Create th.conf

* Update th.conf
2023-04-14 09:41:24 +02:00
Sofiane H. Djerbi bd4944fb2f
Fix typo in FR translation (#411) 2023-04-13 23:55:26 +02:00
Sofiane H. Djerbi 2c6f19aac1
Complete FR translation (#410) 2023-04-13 14:17:27 +02:00
Lukas Rieger (Blue) b3875597a4
Generalize MCA implementation a bit more 2023-04-12 18:06:50 +02:00
Lukas Rieger (Blue) 8a85deb630
Fix publishing task upload file 2023-04-05 00:20:58 +02:00
Lukas Rieger (Blue) b6fe5cf692
Fix publishing task 2023-04-05 00:16:48 +02:00
Lukas Rieger (Blue) c142c823d3
Add hangar-publishing 2023-04-05 00:12:09 +02:00
1260 changed files with 23747 additions and 27887 deletions

1
.gitattributes vendored
View File

@ -4,6 +4,7 @@
*.bat text eol=crlf
gradlew text eol=lf
*.sh text eol=lf
*.conf text eol=lf
*.java text
*.java diff=java

View File

@ -19,7 +19,7 @@ So, if something doesn't work because it is not implemented yet, its not a bug.
If you are not sure, you can briefly ask about it in our [Discord](https://discord.gg/zmkyJa3) before creating an Issue. :)
- Make sure you tested it well enough to be sure it's not an issue on your end. If something doesn't work for you but for everyone else, its probably **not** a bug!
Also, please make sure noone else has already reported the same or a very similar bug!
Also, please make sure no one else has already reported the same or a very similar bug!
If you have additional information for an existing bug-report, you can add a comment to the already existing Issue :)
To report your bug, please open a [new Issue](https://github.com/BlueMap-Minecraft/BlueMap/issues/new?template=bug_report.md) with the `Bug report`-template and follow these guidlines:
@ -53,7 +53,20 @@ Make sure your Issue is easy to read and not a mess:
Create a separate Issue for each bug you find! Issues that contain more than one bug will be closed!
## Suggesting a new feature or change
**(Todo)**
Please use our [discord](https://discord.gg/zmkyJa3)s #suggestions channel to pitch new ideas.
We will discuss them there and if they are considered, I'll add an issue/note to out [TODO](https://github.com/orgs/BlueMap-Minecraft/projects/2/views/1)-Board!
## Creating a Pull-Request
**(Todo)**
If you want to develop a new PR, please run your Idea by me first in our [discord](https://discord.gg/zmkyJa3)!
We can discuss details there, since I have a lot of future plans in my head that are not written anywhere, and they might need to be considered
when implementing your feature!
*(Also, I tend to be quite picky about certain implementation styles and details ^^')*
**Please keep in mind that any feature you implement will need to be maintained in the future by me.
For this reason I will only accept PR's for features that I deem to be useful, maintainable, in-scope of the project and
worth it's maintenance-workload!**
Ofc the usual "good code quality..." stuff, i think that's common sense.
Try to match the existing code-style.
Don't add new libraries/dependencies without my ok.
Hacky stuff is not allowed =)

View File

@ -1,14 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Why do you want this feature
**Describe the solution you'd like**
A clear and concise description of what you want to happen.

156
.github/translation-checker/index.js vendored Normal file
View File

@ -0,0 +1,156 @@
import { execSync } from "node:child_process";
import { readdirSync } from "node:fs";
import path from "node:path";
// doesn't really matter, just setting something consistant and close enough for us europeans
process.env.TZ = "Europe/Berlin";
function parse(str) {
const blame = execSync(`git blame --porcelain ${str}`).toString("utf8").trim().split("\n");
const commitMap = new Map();
const nodes = [];
const path = [];
let inMultiLineString = false;
let multiLineStringLastUpdated = null;
// let multiLineStringValue = "";
for (let i = 0; i < blame.length; i++) {
const hash = blame[i].split(" ")[0];
i++;
if (!commitMap.has(hash)) {
const commit = {};
let j = 0;
while (true) {
const line = blame[i + j];
if (line[0] === "\t") break;
const [key, ...rest] = line.split(" ");
const val = rest.join(" ");
commit[key.replace(/-\S/g, (s) => s.slice(1).toUpperCase())] = val;
j++;
}
commitMap.set(hash, commit);
i += j;
}
const commit = commitMap.get(hash);
let lastUpdated = parseInt(commit.authorTime);
if (inMultiLineString) {
const line = blame[i].slice(1).trimEnd();
if (line.endsWith('"""')) {
// multiLineStringValue += "\n" + line.slice(0, -3);
nodes.push({
path: [...path],
lastUpdated: multiLineStringLastUpdated,
// value: multiLineStringValue,
});
inMultiLineString = false;
multiLineStringLastUpdated = null;
// multiLineStringValue = "";
path.pop();
continue;
} else {
if (lastUpdated > multiLineStringLastUpdated)
multiLineStringLastUpdated = commit.authorTime;
// multiLineStringValue += "\n" + blame[i].slice(1);
continue;
}
}
const line = blame[i].slice(1).trim();
if (line === "{") continue;
if (line === "}") {
path.pop();
continue;
}
if (!line.includes('"')) {
path.push(line.split(":")[0].split(" ")[0]);
continue;
}
const [key, rest] = line.split(":");
if (rest.trimStart().startsWith('"""')) {
inMultiLineString = true;
multiLineStringLastUpdated = lastUpdated;
// multiLineStringValue = rest.trimStart().slice(3);
path.push(key);
continue;
}
nodes.push({
path: [...path, key],
lastUpdated,
// value: rest.trimStart().slice(1, -1)
});
}
return nodes;
}
const langFolder = "../../BlueMapCommon/webapp/public/lang/";
const languageFiles = readdirSync(langFolder).filter(
(f) => f.endsWith(".conf") && f !== "settings.conf"
);
const languages = languageFiles.map((file) => {
const nodes = parse(path.join(langFolder, file));
const name = file.split(".").reverse().slice(1).reverse().join(".");
return {
name,
nodes,
};
});
const sourceLanguageName = "en";
const sourceLanguage = languages.find((l) => l.name === sourceLanguageName);
if (!sourceLanguage) throw new Error(`Source language "${sourceLanguageName}" not found!`);
languages.splice(languages.indexOf(sourceLanguage), 1);
function diff(source, other) {
const sourceKeys = source.map((n) => n.path.join("."));
const otherKeys = other.map((n) => n.path.join("."));
const missing = sourceKeys.filter((sk) => !otherKeys.includes(sk));
const extra = otherKeys.filter((ok) => !sourceKeys.includes(ok));
const outdated = other
.map((n) => {
const sourceNode = source.find((sn) => sn.path.join(".") === n.path.join("."));
return { ...n, sourceNode };
})
.filter((n) => {
return n.sourceNode && n.sourceNode.lastUpdated > n.lastUpdated;
});
return {
missing,
extra,
outdated,
};
}
const upToDate = [];
for (const { name, nodes } of languages) {
const { missing, extra, outdated } = diff(sourceLanguage.nodes, nodes);
if (missing.length + extra.length + outdated.length === 0) {
upToDate.push(name);
continue;
}
console.log(`=== ${name} ===`);
if (missing.length) {
console.log(`Missing (${missing.length}):`);
for (const key of missing) console.log("-", key);
console.log();
}
if (extra.length) {
console.log(`Extra (${extra.length}):`);
for (const key of extra) console.log("-", key);
console.log();
}
if (outdated.length) {
console.log(`Outdated (${outdated.length}):`);
for (const { path, lastUpdated, sourceNode } of outdated)
console.log(
"-",
path.join("."),
`(updated ${new Date(lastUpdated * 1000).toLocaleString(
"de"
)}, source updated ${new Date(sourceNode.lastUpdated * 1000).toLocaleString("de")})`
);
console.log();
}
}
if (upToDate.length) console.log("Up to date:", upToDate.join(", "));

13
.github/translation-checker/package-lock.json generated vendored Normal file
View File

@ -0,0 +1,13 @@
{
"name": "translation-checker",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "translation-checker",
"version": "1.0.0",
"license": "MIT"
}
}
}

View File

@ -0,0 +1,11 @@
{
"name": "translation-checker",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"type": "module",
"scripts": {
"start": "node ."
}
}

View File

@ -9,6 +9,7 @@ on:
pull_request:
branches:
- "*"
workflow_dispatch:
jobs:
build:
@ -18,20 +19,17 @@ jobs:
with:
submodules: recursive
fetch-depth: 0 # needed for versioning
- name: Set up JDK 1.17
uses: actions/setup-java@v1
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: 17
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: "${{ runner.os }}-bluemap-${{ hashFiles('**/*.gradle*') }}"
restore-keys: |
${{ runner.os }}-bluemap-
distribution: 'temurin'
java-version: |
16
17
21
cache: 'gradle'
- name: Build with Gradle
run: ./gradlew clean test build
run: ./gradlew clean spotlessCheck test build
- uses: actions/upload-artifact@v2
with:
name: artifacts
@ -75,6 +73,7 @@ jobs:
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
push: ${{ github.event_name != 'pull_request' }}

View File

@ -2,29 +2,29 @@ name: Publish
on:
workflow_dispatch:
push:
tags:
- "**"
jobs:
publish:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0 # needed for versioning
- name: Set up JDK 1.17
uses: actions/setup-java@v1
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: 17
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: "${{ runner.os }}-bluemap-${{ hashFiles('**/*.gradle*') }}"
restore-keys: |
${{ runner.os }}-bluemap-
distribution: 'temurin'
java-version: |
16
17
21
cache: 'gradle'
- name: Build with Gradle
run: ./gradlew clean :BlueMapCore:publish :BlueMapCommon:publish
env:
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
run: ./gradlew publish
BLUECOLORED_USERNAME: ${{ secrets.BLUECOLORED_USERNAME }}
BLUECOLORED_PASSWORD: ${{ secrets.BLUECOLORED_PASSWORD }}

View File

@ -0,0 +1,26 @@
name: Check translations
on:
push:
paths:
- "BlueMapCommon/webapp/public/lang/**"
- ".github/translation-checker/**"
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install deps
working-directory: .github/translation-checker
run: npm ci
- name: Run Translation Checker
working-directory: .github/translation-checker
run: npm start

4
.gitignore vendored
View File

@ -14,9 +14,11 @@ node_modules/
*.launch
release.md
# exclude generated resource
BlueMapCommon/src/main/resources/de/bluecolored/bluemap/webapp.zip
BlueMapCore/src/main/resources/de/bluecolored/bluemap/*/resourceExtensions.zip
BlueMapCore/src/main/resources/de/bluecolored/bluemap/resourceExtensions.zip
#exclude-test-data
data/test-render

@ -1 +1 @@
Subproject commit 812e4b8fce15796c5d67c5f0ff8121a6d7e7657b
Subproject commit ec977113495dacd6f2e24239015f4b94b305fc52

View File

@ -9,10 +9,11 @@ plugins {
id ("com.github.node-gradle.node") version "3.5.0"
}
group = "de.bluecolored.bluemap.common"
version = "0.0.0"
group = "de.bluecolored.bluemap"
version = System.getProperty("bluemap.version") ?: "?" // set by BlueMapCore
val lastVersion = System.getProperty("bluemap.lastVersion") ?: "?" // set by BlueMapCore
val javaTarget = 11
val javaTarget = 16
java {
sourceCompatibility = JavaVersion.toVersion(javaTarget)
targetCompatibility = JavaVersion.toVersion(javaTarget)
@ -20,19 +21,19 @@ java {
repositories {
mavenCentral()
maven {
setUrl("https://libraries.minecraft.net")
}
maven {
setUrl("https://jitpack.io")
}
maven ("https://libraries.minecraft.net")
maven ("https://repo.bluecolored.de/releases")
}
dependencies {
api ("com.mojang:brigadier:1.0.17")
api ("de.bluecolored.bluemap.core:BlueMapCore")
api ("de.bluecolored.bluemap:BlueMapCore")
compileOnly ("org.jetbrains:annotations:16.0.2")
compileOnly ("org.projectlombok:lombok:1.18.32")
annotationProcessor ("org.projectlombok:lombok:1.18.32")
testImplementation ("org.junit.jupiter:junit-jupiter:5.8.2")
testRuntimeOnly ("org.junit.jupiter:junit-jupiter-engine:5.8.2")
@ -69,12 +70,14 @@ tasks.test {
useJUnitPlatform()
}
tasks.register("buildWebapp", type = NpmTask::class) {
tasks.clean {
doFirst {
if (!file("webapp/dist/").deleteRecursively())
throw IOException("Failed to delete build directory!")
}
}
tasks.register("buildWebapp", type = NpmTask::class) {
dependsOn ("npmInstall")
args.set(listOf("run", "build"))
@ -88,7 +91,6 @@ tasks.register("zipWebapp", type = Zip::class) {
archiveFileName.set("webapp.zip")
destinationDirectory.set(file("src/main/resources/de/bluecolored/bluemap/"))
//outputs.upToDateWhen { false }
inputs.dir("webapp/dist/")
outputs.file("src/main/resources/de/bluecolored/bluemap/webapp.zip")
}
@ -99,6 +101,20 @@ tasks.processResources {
}
publishing {
repositories {
maven {
name = "bluecolored"
val releasesRepoUrl = "https://repo.bluecolored.de/releases"
val snapshotsRepoUrl = "https://repo.bluecolored.de/snapshots"
url = uri(if (version == lastVersion) releasesRepoUrl else snapshotsRepoUrl)
credentials {
username = project.findProperty("bluecoloredUsername") as String? ?: System.getenv("BLUECOLORED_USERNAME")
password = project.findProperty("bluecoloredPassword") as String? ?: System.getenv("BLUECOLORED_PASSWORD")
}
}
}
publications {
create<MavenPublication>("maven") {
groupId = project.group.toString()
@ -106,6 +122,12 @@ publishing {
version = project.version.toString()
from(components["java"])
versionMapping {
usage("java-api") {
fromResolutionOf("runtimeClasspath")
}
}
}
}
}

Binary file not shown.

View File

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
BlueMapCommon/gradlew vendored
View File

@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -26,10 +26,15 @@ package de.bluecolored.bluemap.common;
import de.bluecolored.bluemap.common.config.*;
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.Map;
public interface BlueMapConfigProvider {
public interface BlueMapConfiguration {
@Nullable String getMinecraftVersion();
CoreConfig getCoreConfig();
WebappConfig getWebappConfig();
@ -42,4 +47,8 @@ public interface BlueMapConfigProvider {
Map<String, StorageConfig> getStorageConfigs();
@Nullable Path getPacksFolder();
@Nullable Path getModsFolder();
}

View File

@ -29,25 +29,24 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.api.gson.MarkerGson;
import de.bluecolored.bluemap.api.markers.MarkerSet;
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.common.config.MapConfig;
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.debug.StateDumper;
import de.bluecolored.bluemap.common.debug.StateDumper;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.mca.MCAWorld;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.MinecraftVersion;
import de.bluecolored.bluemap.core.resources.VersionManifest;
import de.bluecolored.bluemap.core.resources.pack.datapack.DataPack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.World;
import org.apache.commons.io.FileUtils;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
@ -60,96 +59,43 @@ import java.lang.reflect.Type;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* This is the attempt to generalize as many actions as possible to have CLI and Plugins run on the same general setup-code.
*/
@DebugDump
public class BlueMapService implements Closeable {
private final ServerInterface serverInterface;
private final BlueMapConfigProvider configs;
private final BlueMapConfiguration config;
private final WebFilesManager webFilesManager;
private final Map<Path, String> worldIds;
private MinecraftVersion minecraftVersion;
private ResourcePack resourcePack;
private final Map<String, World> worlds;
private final Map<String, BmMap> maps;
private final Map<String, Storage> storages;
private volatile WebFilesManager webFilesManager;
private Map<String, World> worlds;
private Map<String, BmMap> maps;
private ResourcePack resourcePack;
public BlueMapService(ServerInterface serverInterface, BlueMapConfigProvider configProvider, @Nullable ResourcePack preloadedResourcePack) {
this(serverInterface, configProvider);
if (preloadedResourcePack != null)
this.resourcePack = preloadedResourcePack;
public BlueMapService(BlueMapConfiguration configuration, @Nullable ResourcePack preloadedResourcePack) {
this(configuration);
this.resourcePack = preloadedResourcePack;
}
public BlueMapService(ServerInterface serverInterface, BlueMapConfigProvider configProvider) {
this.serverInterface = serverInterface;
this.configs = configProvider;
public BlueMapService(BlueMapConfiguration configuration) {
this.config = configuration;
this.webFilesManager = new WebFilesManager(config.getWebappConfig().getWebroot());
this.worldIds = new ConcurrentHashMap<>();
this.storages = new HashMap<>();
this.worlds = new ConcurrentHashMap<>();
this.maps = new ConcurrentHashMap<>();
this.storages = new ConcurrentHashMap<>();
StateDumper.global().register(this);
}
public String getWorldId(Path worldFolder) throws IOException {
// fast-path
String id = worldIds.get(worldFolder);
if (id != null) return id;
// second try with normalized absolute path
worldFolder = worldFolder.toAbsolutePath().normalize();
id = worldIds.get(worldFolder);
if (id != null) return id;
// secure (slower) query with real path
worldFolder = worldFolder.toRealPath();
id = worldIds.get(worldFolder);
if (id != null) return id;
synchronized (worldIds) {
// check again if another thread has already added the world
id = worldIds.get(worldFolder);
if (id != null) return id;
Logger.global.logDebug("Loading world id for '" + worldFolder + "'...");
// now we can be sure it wasn't loaded yet .. load
Path idFile = worldFolder.resolve("bluemap.id");
if (!Files.exists(idFile)) {
id = this.serverInterface.getWorld(worldFolder)
.flatMap(ServerWorld::getId)
.orElse(UUID.randomUUID().toString());
Files.writeString(idFile, id, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
worldIds.put(worldFolder, id);
return id;
}
id = Files.readString(idFile);
worldIds.put(worldFolder, id);
return id;
}
}
public WebFilesManager getWebFilesManager() {
if (webFilesManager == null) {
synchronized (this) {
if (webFilesManager == null)
webFilesManager = new WebFilesManager(configs.getWebappConfig().getWebroot());
}
}
return webFilesManager;
}
@ -163,13 +109,13 @@ public class BlueMapService implements Closeable {
}
// update settings.json
if (!configs.getWebappConfig().isUpdateSettingsFile()) {
if (!config.getWebappConfig().isUpdateSettingsFile()) {
webFilesManager.loadSettings();
webFilesManager.addFrom(configs.getWebappConfig());
webFilesManager.addFrom(config.getWebappConfig());
} else {
webFilesManager.setFrom(configs.getWebappConfig());
webFilesManager.setFrom(config.getWebappConfig());
}
for (String mapId : configs.getMapConfigs().keySet()) {
for (String mapId : config.getMapConfigs().keySet()) {
webFilesManager.addMap(mapId);
}
webFilesManager.saveSettings();
@ -179,23 +125,45 @@ public class BlueMapService implements Closeable {
}
}
public synchronized Map<String, World> getWorlds() throws InterruptedException {
if (worlds == null) loadWorldsAndMaps();
return worlds;
/**
* Gets all loaded maps.
* @return A map of loaded maps
*/
public Map<String, BmMap> getMaps() {
return Collections.unmodifiableMap(maps);
}
public synchronized Map<String, BmMap> getMaps() throws InterruptedException {
if (maps == null) loadWorldsAndMaps();
return maps;
/**
* Gets all loaded worlds.
* @return A map of loaded worlds
*/
public Map<String, World> getWorlds() {
return Collections.unmodifiableMap(worlds);
}
private synchronized void loadWorldsAndMaps() throws InterruptedException {
maps = new HashMap<>();
worlds = new HashMap<>();
/**
* Gets or loads configured maps.
* @return A map of loaded maps
*/
public Map<String, BmMap> getOrLoadMaps() throws InterruptedException {
return getOrLoadMaps(mapId -> true);
}
/**
* Gets or loads configured maps.
* @param filter A predicate filtering map-ids that should be loaded
* (if maps are already loaded, they will be returned as well)
* @return A map of all loaded maps
*/
public synchronized Map<String, BmMap> getOrLoadMaps(Predicate<String> filter) throws InterruptedException {
for (var entry : config.getMapConfigs().entrySet()) {
if (Thread.interrupted()) throw new InterruptedException();
if (!filter.test(entry.getKey())) continue;
if (maps.containsKey(entry.getKey())) continue;
for (var entry : configs.getMapConfigs().entrySet()) {
try {
loadMapConfig(entry.getKey(), entry.getValue());
loadMap(entry.getKey(), entry.getValue());
} catch (ConfigurationException ex) {
Logger.global.logWarning(ex.getFormattedExplanation());
Throwable cause = ex.getRootCause();
@ -204,16 +172,15 @@ public class BlueMapService implements Closeable {
}
}
}
worlds = Collections.unmodifiableMap(worlds);
maps = Collections.unmodifiableMap(maps);
return Collections.unmodifiableMap(maps);
}
private synchronized void loadMapConfig(String id, MapConfig mapConfig) throws ConfigurationException, InterruptedException {
private synchronized void loadMap(String id, MapConfig mapConfig) throws ConfigurationException, InterruptedException {
String name = mapConfig.getName();
if (name == null) name = id;
Path worldFolder = mapConfig.getWorld();
Key dimension = mapConfig.getDimension();
// if there is no world configured, we assume the map is static, or supplied from a different server
if (worldFolder == null) {
@ -221,48 +188,63 @@ public class BlueMapService implements Closeable {
return;
}
// if there is no dimension configured, we assume world-folder is actually the dimension-folder and convert (backwards compatibility)
if (dimension == null) {
worldFolder = worldFolder.normalize();
if (worldFolder.endsWith("DIM-1")) {
worldFolder = worldFolder.getParent();
dimension = DataPack.DIMENSION_THE_NETHER;
} else if (worldFolder.endsWith("DIM1")) {
worldFolder = worldFolder.getParent();
dimension = DataPack.DIMENSION_THE_END;
} else if (
worldFolder.getNameCount() > 3 &&
worldFolder.getName(worldFolder.getNameCount() - 3).toString().equals("dimensions")
) {
String namespace = worldFolder.getName(worldFolder.getNameCount() - 2).toString();
String value = worldFolder.getName(worldFolder.getNameCount() - 1).toString();
worldFolder = worldFolder.subpath(0, worldFolder.getNameCount() - 3);
dimension = new Key(namespace, value);
} else {
dimension = DataPack.DIMENSION_OVERWORLD;
}
Logger.global.logInfo("The map '" + name + "' has no dimension configured.\n" +
"Assuming world: '" + worldFolder + "' and dimension: '" + dimension + "'.");
}
if (!Files.isDirectory(worldFolder)) {
throw new ConfigurationException(
"'" + worldFolder.toAbsolutePath().normalize() + "' does not exist or is no directory!\n" +
"Check if the 'world' setting in the config-file for that map is correct, or remove the entire config-file if you don't want that map.");
}
String worldId;
try {
worldId = getWorldId(worldFolder);
} catch (IOException ex) {
throw new ConfigurationException(
"Could not load the ID for the world (" + worldFolder.toAbsolutePath().normalize() + ")!\n" +
"Make sure BlueMap has read and write access/permissions to the world-files for this map.",
ex);
}
String worldId = World.id(worldFolder, dimension);
World world = worlds.get(worldId);
if (world == null) {
try {
Logger.global.logInfo("Loading world '" + worldId + "' (" + worldFolder.toAbsolutePath().normalize() + ")...");
world = new MCAWorld(worldFolder, mapConfig.getWorldSkyLight(), mapConfig.isIgnoreMissingLightData());
Logger.global.logDebug("Loading world " + worldId + " ...");
world = MCAWorld.load(worldFolder, dimension, loadDataPack(worldFolder));
worlds.put(worldId, world);
} catch (IOException ex) {
throw new ConfigurationException(
"Failed to load world '" + worldId + "' (" + worldFolder.toAbsolutePath().normalize() + ")!\n" +
"Failed to load world " + worldId + "!\n" +
"Is the level.dat of that world present and not corrupted?",
ex);
}
}
Storage storage = getStorage(mapConfig.getStorage());
Storage storage = getOrLoadStorage(mapConfig.getStorage());
try {
Logger.global.logInfo("Loading map '" + name + "'...");
Logger.global.logInfo("Loading map '" + id + "'...");
BmMap map = new BmMap(
id,
name,
worldId,
world,
storage,
getResourcePack(),
storage.map(id),
getOrLoadResourcePack(),
mapConfig
);
maps.put(id, map);
@ -293,18 +275,18 @@ public class BlueMapService implements Closeable {
}
}
public synchronized Storage getStorage(String storageId) throws ConfigurationException {
public synchronized Storage getOrLoadStorage(String storageId) throws ConfigurationException, InterruptedException {
Storage storage = storages.get(storageId);
if (storage == null) {
try {
StorageConfig storageConfig = getConfigs().getStorageConfigs().get(storageId);
StorageConfig storageConfig = getConfig().getStorageConfigs().get(storageId);
if (storageConfig == null) {
throw new ConfigurationException("There is no storage-configuration for '" + storageId + "'!\n" +
"You will either need to define that storage, or change the map-config to use a storage-config that exists.");
}
Logger.global.logInfo("Initializing Storage: '" + storageId + "' (Type: " + storageConfig.getStorageType() + ")");
Logger.global.logInfo("Initializing Storage: '" + storageId + "' (Type: '" + storageConfig.getStorageType().getKey() + "')");
storage = storageConfig.createStorage();
storage.initialize();
@ -331,130 +313,156 @@ public class BlueMapService implements Closeable {
return storage;
}
public synchronized ResourcePack getResourcePack() throws ConfigurationException, InterruptedException {
public @Nullable ResourcePack getResourcePack() {
return resourcePack;
}
public synchronized ResourcePack getOrLoadResourcePack() throws ConfigurationException, InterruptedException {
if (resourcePack == null) {
MinecraftVersion minecraftVersion = serverInterface.getMinecraftVersion();
MinecraftVersion minecraftVersion = getOrLoadMinecraftVersion();
Path vanillaResourcePack = minecraftVersion.getResourcePack();
Path defaultResourceFile = configs.getCoreConfig().getData().resolve("minecraft-client-" + minecraftVersion.getResource().getVersion().getVersionString() + ".jar");
Path resourceExtensionsFile = configs.getCoreConfig().getData().resolve("resourceExtensions.zip");
if (Thread.interrupted()) throw new InterruptedException();
Path resourcePackFolder = serverInterface.getConfigFolder().resolve("resourcepacks");
Deque<Path> packRoots = getPackRoots();
packRoots.addLast(vanillaResourcePack);
try {
FileHelper.createDirectories(resourcePackFolder);
} catch (IOException ex) {
throw new ConfigurationException(
"BlueMap failed to create this folder:\n" +
resourcePackFolder + "\n" +
"Does BlueMap has sufficient permissions?",
ex);
}
if (!Files.exists(defaultResourceFile)) {
if (configs.getCoreConfig().isAcceptDownload()) {
//download file
try {
Logger.global.logInfo("Downloading " + minecraftVersion.getResource().getClientUrl() + " to " + defaultResourceFile + " ...");
FileHelper.createDirectories(defaultResourceFile.getParent());
Path tempResourceFile = defaultResourceFile.getParent().resolve(defaultResourceFile.getFileName() + ".filepart");
Files.deleteIfExists(tempResourceFile);
FileUtils.copyURLToFile(new URL(minecraftVersion.getResource().getClientUrl()), tempResourceFile.toFile(), 10000, 10000);
FileHelper.move(tempResourceFile, defaultResourceFile);
} catch (IOException ex) {
throw new ConfigurationException("Failed to download resources!", ex);
}
} else {
throw new MissingResourcesException();
}
}
try {
Files.deleteIfExists(resourceExtensionsFile);
FileHelper.createDirectories(resourceExtensionsFile.getParent());
URL resourceExtensionsUrl = Objects.requireNonNull(
Plugin.class.getResource(
"/de/bluecolored/bluemap/" + minecraftVersion.getResource().getResourcePrefix() +
"/resourceExtensions.zip")
);
FileUtils.copyURLToFile(resourceExtensionsUrl, resourceExtensionsFile.toFile(), 10000, 10000);
} catch (IOException ex) {
throw new ConfigurationException(
"Failed to create resourceExtensions.zip!\n" +
"Does BlueMap has sufficient write permissions?",
ex);
}
try {
resourcePack = new ResourcePack();
List<Path> resourcePackRoots = new ArrayList<>();
// load from resourcepack folder
try (Stream<Path> resourcepackFiles = Files.list(resourcePackFolder)) {
resourcepackFiles
.sorted(Comparator.reverseOrder())
.forEach(resourcePackRoots::add);
}
if (configs.getCoreConfig().isScanForModResources()) {
// load from mods folder
Path modsFolder = serverInterface.getModsFolder().orElse(null);
if (modsFolder != null && Files.isDirectory(modsFolder)) {
try (Stream<Path> resourcepackFiles = Files.list(modsFolder)) {
resourcepackFiles
.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().endsWith(".jar"))
.forEach(resourcePackRoots::add);
}
}
// load from datapacks
for (Path worldFolder : getWorldFolders()) {
Path datapacksFolder = worldFolder.resolve("datapacks");
if (!Files.isDirectory(datapacksFolder)) continue;
try (Stream<Path> resourcepackFiles = Files.list(worldFolder.resolve("datapacks"))) {
resourcepackFiles.forEach(resourcePackRoots::add);
}
}
}
resourcePackRoots.add(resourceExtensionsFile);
resourcePackRoots.add(defaultResourceFile);
resourcePack.loadResources(resourcePackRoots);
ResourcePack resourcePack = new ResourcePack(minecraftVersion.getResourcePackVersion());
resourcePack.loadResources(packRoots);
this.resourcePack = resourcePack;
} catch (IOException | RuntimeException e) {
throw new ConfigurationException("Failed to parse resources!\n" +
"Is one of your resource-packs corrupted?", e);
}
}
return resourcePack;
return this.resourcePack;
}
public Optional<ResourcePack> getResourcePackIfLoaded() {
return Optional.ofNullable(this.resourcePack);
}
public synchronized DataPack loadDataPack(Path worldFolder) throws ConfigurationException, InterruptedException {
MinecraftVersion minecraftVersion = getOrLoadMinecraftVersion();
Path vanillaDataPack = minecraftVersion.getDataPack();
private Collection<Path> getWorldFolders() {
Set<Path> folders = new HashSet<>();
for (MapConfig mapConfig : configs.getMapConfigs().values()) {
Path folder = mapConfig.getWorld();
if (folder == null) continue;
folder = folder.toAbsolutePath().normalize();
if (Files.isDirectory(folder)) {
folders.add(folder);
if (Thread.interrupted()) throw new InterruptedException();
// also load world datapacks
Iterable<Path> worldPacks = List.of();
Path worldPacksFolder = worldFolder.resolve("datapacks");
if (Files.isDirectory(worldPacksFolder)) {
try (Stream<Path> worldPacksStream = Files.list(worldPacksFolder)) {
worldPacks = worldPacksStream.toList();
} catch (IOException e) {
throw new ConfigurationException("Failed to access the worlds datapacks folder.", e);
}
}
return folders;
Deque<Path> packRoots = getPackRoots(worldPacks);
packRoots.addLast(vanillaDataPack);
try {
DataPack datapack = new DataPack(minecraftVersion.getDataPackVersion());
datapack.loadResources(packRoots);
return datapack;
} catch (IOException | RuntimeException e) {
throw new ConfigurationException("Failed to parse resources!\n" +
"Is one of your resource-packs corrupted?", e);
}
}
public BlueMapConfigProvider getConfigs() {
return configs;
private synchronized Deque<Path> getPackRoots(Path... additionalRoots) throws ConfigurationException, InterruptedException {
return getPackRoots(List.of(additionalRoots));
}
private synchronized Deque<Path> getPackRoots(Iterable<Path> additionalRoots) throws ConfigurationException, InterruptedException {
@Nullable Path packsFolder = config.getPacksFolder();
@Nullable Path modsFolder = config.getModsFolder();
try {
FileHelper.createDirectories(packsFolder);
} catch (IOException ex) {
throw new ConfigurationException(
"BlueMap failed to create this folder:\n" +
packsFolder + "\n" +
"Does BlueMap have sufficient permissions?",
ex);
}
Path resourceExtensionsFile = config.getCoreConfig().getData().resolve("resourceExtensions.zip");
if (Thread.interrupted()) throw new InterruptedException();
try {
Files.deleteIfExists(resourceExtensionsFile);
FileHelper.createDirectories(resourceExtensionsFile.getParent());
URL resourceExtensionsUrl = Objects.requireNonNull(
Plugin.class.getResource("/de/bluecolored/bluemap/resourceExtensions.zip")
);
FileHelper.copy(resourceExtensionsUrl, resourceExtensionsFile);
} catch (IOException ex) {
throw new ConfigurationException(
"Failed to create resourceExtensions.zip!\n" +
"Does BlueMap has sufficient write permissions?",
ex);
}
Deque<Path> packRoots = new LinkedList<>();
// load from pack folder
if (packsFolder != null && Files.isDirectory(packsFolder)) {
try (Stream<Path> packFiles = Files.list(packsFolder)) {
packFiles
.sorted(Comparator.reverseOrder())
.forEach(packRoots::add);
} catch (IOException e) {
throw new ConfigurationException("Failed to access packs folder.", e);
}
}
// add additional roots
additionalRoots.forEach(packRoots::add);
// load from mods folder
if (config.getCoreConfig().isScanForModResources() && modsFolder != null && Files.isDirectory(modsFolder)) {
try (Stream<Path> packFiles = Files.list(modsFolder)) {
packFiles
.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().endsWith(".jar"))
.forEach(packRoots::add);
} catch (IOException e) {
throw new ConfigurationException("Failed to access packs folder.", e);
}
}
packRoots.add(resourceExtensionsFile);
return packRoots;
}
public synchronized MinecraftVersion getOrLoadMinecraftVersion() throws ConfigurationException {
if (this.minecraftVersion == null) {
try {
this.minecraftVersion = MinecraftVersion.load(
config.getMinecraftVersion(),
config.getCoreConfig().getData(),
config.getCoreConfig().isAcceptDownload()
);
} catch (IOException ex) {
if (!config.getCoreConfig().isAcceptDownload()) {
throw new MissingResourcesException();
} else {
throw new ConfigurationException("""
BlueMap was not able to download some important resources!
Make sure BlueMap is able to connect to mojang-servers (%s)."""
.formatted(VersionManifest.DOMAIN), ex);
}
}
}
return this.minecraftVersion;
}
public BlueMapConfiguration getConfig() {
return config;
}
@Override

View File

@ -24,28 +24,31 @@
*/
package de.bluecolored.bluemap.common;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import de.bluecolored.bluemap.common.config.WebappConfig;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.util.FileHelper;
import org.apache.commons.io.FileUtils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.*;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.HashSet;
import java.util.Set;
public class WebFilesManager {
private static final Gson GSON = ResourcesGson.addAdapter(new GsonBuilder())
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
//.setPrettyPrinting() // enable pretty printing for easy editing
.create();
private final Path webRoot;
private Settings settings;
@ -60,7 +63,7 @@ public class WebFilesManager {
public void loadSettings() throws IOException {
try (BufferedReader reader = Files.newBufferedReader(getSettingsFile())) {
this.settings = ResourcesGson.INSTANCE.fromJson(reader, Settings.class);
this.settings = GSON.fromJson(reader, Settings.class);
}
}
@ -68,10 +71,7 @@ public class WebFilesManager {
FileHelper.createDirectories(getSettingsFile().getParent());
try (BufferedWriter writer = Files.newBufferedWriter(getSettingsFile(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
ResourcesGson.addAdapter(new GsonBuilder())
.setPrettyPrinting() // enable pretty printing for easy editing
.create()
.toJson(this.settings, writer);
GSON.toJson(this.settings, writer);
}
}
@ -108,42 +108,20 @@ public class WebFilesManager {
}
public void updateFiles() throws IOException {
URL fileResource = getClass().getResource("/de/bluecolored/bluemap/webapp.zip");
File tempFile = File.createTempFile("bluemap_webroot_extraction", null);
URL zippedWebapp = getClass().getResource("/de/bluecolored/bluemap/webapp.zip");
if (zippedWebapp == null) throw new IOException("Failed to open bundled webapp.");
if (fileResource == null) throw new IOException("Failed to open bundled webapp.");
// extract zip to webroot
FileHelper.extractZipFile(zippedWebapp, webRoot, StandardCopyOption.REPLACE_EXISTING);
try {
FileUtils.copyURLToFile(fileResource, tempFile, 10000, 10000);
try (ZipFile zipFile = new ZipFile(tempFile)){
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while(entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
if (zipEntry.isDirectory()) {
File dir = webRoot.resolve(zipEntry.getName()).toFile();
FileUtils.forceMkdir(dir);
} else {
File target = webRoot.resolve(zipEntry.getName()).toFile();
FileUtils.forceMkdirParent(target);
FileUtils.copyInputStreamToFile(zipFile.getInputStream(zipEntry), target);
}
}
}
// set version in index.html
Path indexFile = webRoot.resolve("index.html");
String indexContent = Files.readString(indexFile);
indexContent = indexContent.replace("%version%", BlueMap.VERSION);
Files.writeString(indexFile, indexContent);
} finally {
if (!tempFile.delete()) {
Logger.global.logWarning("Failed to delete file: " + tempFile);
}
}
// set version in index.html
Path indexFile = webRoot.resolve("index.html");
String indexContent = Files.readString(indexFile);
indexContent = indexContent.replace("%version%", BlueMap.VERSION);
Files.writeString(indexFile, indexContent);
}
@SuppressWarnings("all")
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal", "unused", "MismatchedQueryAndUpdateOfCollection"})
private static class Settings {
private String version = BlueMap.VERSION;
@ -151,6 +129,7 @@ public class WebFilesManager {
private boolean useCookies = true;
private boolean enableFreeFlight = true;
private boolean defaultToFlatView = false;
private String startLocation = null;
@ -174,6 +153,7 @@ public class WebFilesManager {
public void setFrom(WebappConfig config) {
this.useCookies = config.isUseCookies();
this.enableFreeFlight = config.isEnableFreeFlight();
this.defaultToFlatView = config.isDefaultToFlatView();
this.startLocation = config.getStartLocation().orElse(null);
this.resolutionDefault = config.getResolutionDefault();
@ -195,8 +175,17 @@ public class WebFilesManager {
}
public void addFrom(WebappConfig config) {
this.scripts.addAll(config.getScripts());
this.styles.addAll(config.getStyles());
Set<String> scripts = config.getScripts();
for (String script : scripts) {
this.scripts.add(script);
Logger.global.logDebug("Registering script from Webapp Config: " + script);
}
Set<String> styles = config.getStyles();
for (String style : styles) {
this.styles.add(style);
Logger.global.logDebug("Registering style from Webapp Config: " + style);
}
}
}

View File

@ -0,0 +1,30 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.addons;
import lombok.experimental.StandardException;
@StandardException
public class AddonException extends Exception {}

View File

@ -22,18 +22,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
package de.bluecolored.bluemap.common.addons;
import java.io.IOException;
import lombok.Getter;
public interface TileInfo {
@Getter
public class AddonInfo {
public static final String ADDON_INFO_FILE = "bluemap.addon.json";
CompressedInputStream readMapTile() throws IOException;
Compression getCompression();
long getSize();
long getLastModified();
private String id;
private String entrypoint;
}

View File

@ -0,0 +1,164 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.addons;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import static de.bluecolored.bluemap.common.addons.AddonInfo.ADDON_INFO_FILE;
public final class Addons {
private static final Gson GSON = new GsonBuilder().create();
private static final Map<String, LoadedAddon> LOADED_ADDONS = new ConcurrentHashMap<>();
private Addons() {
throw new UnsupportedOperationException("Utility class");
}
public static void tryLoadAddons(Path root) {
tryLoadAddons(root, false);
}
public static void tryLoadAddons(Path root, boolean expectOnlyAddons) {
if (!Files.exists(root)) return;
try (Stream<Path> files = Files.list(root)) {
files
.filter(Files::isRegularFile)
.filter(f -> f.getFileName().toString().endsWith(".jar"))
.forEach(expectOnlyAddons ? Addons::tryLoadAddon : Addons::tryLoadJar);
} catch (IOException e) {
Logger.global.logError("Failed to load addons from '%s'".formatted(root), e);
}
}
public static void tryLoadAddon(Path addonJarFile) {
try {
AddonInfo addonInfo = loadAddonInfo(addonJarFile);
if (addonInfo == null) throw new AddonException("No %s found in '%s'".formatted(ADDON_INFO_FILE, addonJarFile));
if (LOADED_ADDONS.containsKey(addonInfo.getId())) return;
loadAddon(addonJarFile, addonInfo);
} catch (IOException | AddonException e) {
Logger.global.logError("Failed to load addon '%s'".formatted(addonJarFile), e);
}
}
public static void tryLoadJar(Path addonJarFile) {
try {
AddonInfo addonInfo = loadAddonInfo(addonJarFile);
if (addonInfo == null) {
Logger.global.logDebug("No %s found in '%s', skipping...".formatted(ADDON_INFO_FILE, addonJarFile));
return;
}
if (LOADED_ADDONS.containsKey(addonInfo.getId())) return;
loadAddon(addonJarFile, addonInfo);
} catch (IOException | AddonException e) {
Logger.global.logError("Failed to load addon '%s'".formatted(addonJarFile), e);
}
}
public synchronized static void loadAddon(Path jarFile, AddonInfo addonInfo) throws AddonException {
Logger.global.logInfo("Loading BlueMap Addon: %s (%s)".formatted(addonInfo.getId(), jarFile));
if (LOADED_ADDONS.containsKey(addonInfo.getId()))
throw new AddonException("Addon with id '%s' is already loaded".formatted(addonInfo.getId()));
try {
ClassLoader addonClassLoader = BlueMap.class.getClassLoader();
Class<?> entrypointClass;
// try to find entrypoint class and load jar with new classloader if needed
try {
entrypointClass = addonClassLoader.loadClass(addonInfo.getEntrypoint());
} catch (ClassNotFoundException e) {
addonClassLoader = new URLClassLoader(
new URL[]{ jarFile.toUri().toURL() },
BlueMap.class.getClassLoader()
);
entrypointClass = addonClassLoader.loadClass(addonInfo.getEntrypoint());
}
// create addon instance
Object instance = entrypointClass.getConstructor().newInstance();
LoadedAddon addon = new LoadedAddon(
addonInfo,
addonClassLoader,
instance
);
LOADED_ADDONS.put(addonInfo.getId(), addon);
// run addon
if (instance instanceof Runnable runnable)
runnable.run();
} catch (Exception e) {
throw new AddonException("Failed to load addon '%s'".formatted(jarFile), e);
}
}
public static @Nullable AddonInfo loadAddonInfo(Path addonJarFile) throws IOException, AddonException {
try (FileSystem fileSystem = FileSystems.newFileSystem(addonJarFile, (ClassLoader) null)) {
for (Path root : fileSystem.getRootDirectories()) {
Path addonInfoFile = root.resolve(ADDON_INFO_FILE);
if (!Files.exists(addonInfoFile)) continue;
try (Reader reader = Files.newBufferedReader(addonInfoFile, StandardCharsets.UTF_8)) {
AddonInfo addonInfo = GSON.fromJson(reader, AddonInfo.class);
if (addonInfo.getId() == null)
throw new AddonException("'id' is missing");
if (addonInfo.getEntrypoint() == null)
throw new AddonException("'entrypoint' is missing");
return addonInfo;
}
}
}
return null;
}
}

View File

@ -0,0 +1,31 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.addons;
public record LoadedAddon (
AddonInfo addonInfo,
ClassLoader classLoader,
Object instance
) {}

View File

@ -25,7 +25,8 @@
package de.bluecolored.bluemap.common.api;
import de.bluecolored.bluemap.api.AssetStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -34,39 +35,39 @@ import java.util.Optional;
public class AssetStorageImpl implements AssetStorage {
private static final String ASSET_PATH = "assets/";
private final Storage storage;
private final MapStorage storage;
private final String mapId;
public AssetStorageImpl(Storage storage, String mapId) {
public AssetStorageImpl(MapStorage storage, String mapId) {
this.storage = storage;
this.mapId = mapId;
}
@Override
public OutputStream writeAsset(String name) throws IOException {
return storage.writeMeta(mapId, ASSET_PATH + name);
return storage.asset(name).write();
}
@Override
public Optional<InputStream> readAsset(String name) throws IOException {
return storage.readMeta(mapId, ASSET_PATH + name);
CompressedInputStream in = storage.asset(name).read();
if (in == null) return Optional.empty();
return Optional.of(in.decompress());
}
@Override
public boolean assetExists(String name) throws IOException {
return storage.readMetaInfo(mapId, ASSET_PATH + name).isPresent();
return storage.asset(name).exists();
}
@Override
public String getAssetUrl(String name) {
return "maps/" + mapId + "/" + Storage.escapeMetaName(ASSET_PATH + name);
return "maps/" + mapId + "/assets/" + MapStorage.escapeAssetName(name);
}
@Override
public void deleteAsset(String name) throws IOException {
storage.deleteMeta(mapId, ASSET_PATH + name);
storage.asset(name).delete();
}
}

View File

@ -29,27 +29,44 @@ import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.api.BlueMapAPI;
import de.bluecolored.bluemap.api.BlueMapMap;
import de.bluecolored.bluemap.api.BlueMapWorld;
import de.bluecolored.bluemap.common.BlueMapService;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.world.World;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collection;
import java.util.Objects;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
public class BlueMapAPIImpl extends BlueMapAPI {
private final Plugin plugin;
private final BlueMapService blueMapService;
private final @Nullable Plugin plugin;
private final WebAppImpl webAppImpl;
private final @Nullable RenderManagerImpl renderManagerImpl;
private final @Nullable PluginImpl pluginImpl;
private final LoadingCache<Object, Optional<BlueMapWorld>> worldCache;
private final LoadingCache<String, Optional<BlueMapMap>> mapCache;
public BlueMapAPIImpl(Plugin plugin) {
this(plugin.getBlueMap(), plugin);
}
public BlueMapAPIImpl(BlueMapService blueMapService, @Nullable Plugin plugin) {
this.blueMapService = blueMapService;
this.plugin = plugin;
this.renderManagerImpl = plugin != null ? new RenderManagerImpl(this, plugin) : null;
this.webAppImpl = new WebAppImpl(blueMapService, plugin);
this.pluginImpl = plugin != null ? new PluginImpl(plugin) : null;
this.worldCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.weakKeys()
@ -60,41 +77,23 @@ public class BlueMapAPIImpl extends BlueMapAPI {
.build(this::getMapUncached);
}
@Override
public RenderManagerImpl getRenderManager() {
return new RenderManagerImpl(this, plugin);
}
@Override
public WebAppImpl getWebApp() {
return new WebAppImpl(plugin);
}
@Override
public de.bluecolored.bluemap.api.plugin.Plugin getPlugin() {
return new PluginImpl(plugin);
}
@Override
public Collection<BlueMapMap> getMaps() {
return plugin.getMaps().values().stream()
.map(map -> {
try {
return new BlueMapMapImpl(plugin, map);
} catch (IOException e) {
Logger.global.logError("[API] Failed to create BlueMapMap for map " + map.getId(), e);
return null;
}
})
.filter(Objects::nonNull)
Map<String, BmMap> maps = blueMapService.getMaps();
return maps.keySet().stream()
.map(this::getMap)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toUnmodifiableSet());
}
@Override
public Collection<BlueMapWorld> getWorlds() {
return plugin.getWorlds().values().stream()
.map(world -> getWorld(world).orElse(null))
.filter(Objects::nonNull)
Map<String, World> worlds = blueMapService.getWorlds();
return worlds.keySet().stream()
.map(this::getWorld)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toUnmodifiableSet());
}
@ -105,39 +104,24 @@ public class BlueMapAPIImpl extends BlueMapAPI {
public Optional<BlueMapWorld> getWorldUncached(Object world) {
if (world instanceof UUID) {
var coreWorld = plugin.getWorlds().get(world.toString());
if (coreWorld != null) world = coreWorld;
}
if (world instanceof String) {
var coreWorld = plugin.getWorlds().get(world);
var coreWorld = blueMapService.getWorlds().get(world);
if (coreWorld != null) world = coreWorld;
}
if (world instanceof World) {
var coreWorld = (World) world;
try {
return Optional.of(new BlueMapWorldImpl(plugin, coreWorld));
} catch (IOException e) {
Logger.global.logError("[API] Failed to create BlueMapWorld for world " + coreWorld.getSaveFolder(), e);
}
return Optional.empty();
if (world instanceof World coreWorld) {
return Optional.of(new BlueMapWorldImpl(coreWorld, blueMapService, plugin));
}
var serverWorld = plugin.getServerInterface().getWorld(world).orElse(null);
if (plugin == null) return Optional.empty();
ServerWorld serverWorld = plugin.getServerInterface().getServerWorld(world).orElse(null);
if (serverWorld == null) return Optional.empty();
try {
String id = plugin.getBlueMap().getWorldId(serverWorld.getSaveFolder());
var coreWorld = plugin.getWorlds().get(id);
if (coreWorld == null) return Optional.empty();
World coreWorld = plugin.getWorld(serverWorld);
if (coreWorld == null) return Optional.empty();
return Optional.of(new BlueMapWorldImpl(plugin, coreWorld));
} catch (IOException e) {
Logger.global.logError("[API] Failed to create BlueMapWorld for world " + serverWorld.getSaveFolder(), e);
return Optional.empty();
}
return Optional.of(new BlueMapWorldImpl(coreWorld, blueMapService, plugin));
}
@ -147,13 +131,15 @@ public class BlueMapAPIImpl extends BlueMapAPI {
}
public Optional<BlueMapMap> getMapUncached(String id) {
var map = plugin.getMaps().get(id);
var maps = blueMapService.getMaps();
var map = maps.get(id);
if (map == null) return Optional.empty();
var world = getWorld(map.getWorld()).orElse(null);
if (world == null) return Optional.empty();
return Optional.of(new BlueMapMapImpl(plugin, map, (BlueMapWorldImpl) world));
return Optional.of(new BlueMapMapImpl(map, (BlueMapWorldImpl) world, plugin));
}
@Override
@ -161,10 +147,27 @@ public class BlueMapAPIImpl extends BlueMapAPI {
return BlueMap.VERSION;
}
@Override
public WebAppImpl getWebApp() {
return webAppImpl;
}
@Override
public RenderManagerImpl getRenderManager() {
if (renderManagerImpl == null) throw new UnsupportedOperationException("RenderManager API is not supported on this platform");
return renderManagerImpl;
}
@Override
public de.bluecolored.bluemap.api.plugin.Plugin getPlugin() {
if (pluginImpl == null) throw new UnsupportedOperationException("Plugin API is not supported on this platform");
return pluginImpl;
}
public void register() {
try {
BlueMapAPI.registerInstance(this);
} catch (ExecutionException ex) {
} catch (Exception ex) {
Logger.global.logError("BlueMapAPI: A BlueMapAPI listener threw an exception (onEnable)!", ex.getCause());
}
}
@ -172,9 +175,31 @@ public class BlueMapAPIImpl extends BlueMapAPI {
public void unregister() {
try {
BlueMapAPI.unregisterInstance(this);
} catch (ExecutionException ex) {
} catch (Exception ex) {
Logger.global.logError("BlueMapAPI: A BlueMapAPI listener threw an exception (onDisable)!", ex.getCause());
}
}
/**
* Easy-access method for addons depending on BlueMapCommon:<br>
* <blockquote><pre>
* BlueMapService bluemap = ((BlueMapAPIImpl) blueMapAPI).blueMapService();
* </pre></blockquote>
*/
@SuppressWarnings("unused")
public BlueMapService blueMapService() {
return blueMapService;
}
/**
* Easy-access method for addons depending on BlueMapCommon:<br>
* <blockquote><pre>
* Plugin plugin = ((BlueMapAPIImpl) blueMapAPI).plugin();
* </pre></blockquote>
*/
@SuppressWarnings("unused")
public @Nullable Plugin plugin() {
return plugin;
}
}

View File

@ -25,16 +25,16 @@
package de.bluecolored.bluemap.common.api;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.AssetStorage;
import de.bluecolored.bluemap.api.BlueMapMap;
import de.bluecolored.bluemap.api.BlueMapWorld;
import de.bluecolored.bluemap.api.AssetStorage;
import de.bluecolored.bluemap.api.markers.MarkerSet;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask;
import de.bluecolored.bluemap.core.map.BmMap;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.Objects;
@ -42,29 +42,21 @@ import java.util.function.Predicate;
public class BlueMapMapImpl implements BlueMapMap {
private final WeakReference<Plugin> plugin;
private final String mapId;
private final WeakReference<BmMap> map;
private final BlueMapWorldImpl world;
private final WeakReference<Plugin> plugin;
public BlueMapMapImpl(Plugin plugin, BmMap map) throws IOException {
this.plugin = new WeakReference<>(plugin);
this.map = new WeakReference<>(map);
this.world = new BlueMapWorldImpl(plugin, map.getWorld());
}
public BlueMapMapImpl(Plugin plugin, BmMap map, BlueMapWorldImpl world) {
this.plugin = new WeakReference<>(plugin);
public BlueMapMapImpl(BmMap map, BlueMapWorldImpl world, @Nullable Plugin plugin) {
this.mapId = map.getId();
this.map = new WeakReference<>(map);
this.world = world;
}
public BmMap getBmMap() {
return unpack(map);
this.plugin = new WeakReference<>(plugin);
}
@Override
public String getId() {
return unpack(map).getId();
return mapId;
}
@Override
@ -119,7 +111,9 @@ public class BlueMapMapImpl implements BlueMapMap {
}
private synchronized void unfreeze() {
Plugin plugin = unpack(this.plugin);
Plugin plugin = this.plugin.get();
if (plugin == null) return; // fail silently: not supported on non-plugin platforms
BmMap map = unpack(this.map);
plugin.startWatchingMap(map);
plugin.getPluginState().getMapState(map).setUpdateEnabled(true);
@ -127,7 +121,9 @@ public class BlueMapMapImpl implements BlueMapMap {
}
private synchronized void freeze() {
Plugin plugin = unpack(this.plugin);
Plugin plugin = this.plugin.get();
if (plugin == null) return; // fail silently: not supported on non-plugin platforms
BmMap map = unpack(this.map);
plugin.stopWatchingMap(map);
plugin.getPluginState().getMapState(map).setUpdateEnabled(false);
@ -144,11 +140,39 @@ public class BlueMapMapImpl implements BlueMapMap {
@Override
public boolean isFrozen() {
return !unpack(plugin).getPluginState().getMapState(unpack(map)).isUpdateEnabled();
Plugin plugin = this.plugin.get();
if (plugin == null) return false; // fail silently: not supported on non-plugin platforms
return !plugin.getPluginState().getMapState(unpack(map)).isUpdateEnabled();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlueMapMapImpl that = (BlueMapMapImpl) o;
return mapId.equals(that.mapId);
}
@Override
public int hashCode() {
return mapId.hashCode();
}
private <T> T unpack(WeakReference<T> ref) {
return Objects.requireNonNull(ref.get(), "Reference lost to delegate object. Most likely BlueMap got reloaded and this instance is no longer valid.");
}
/**
* Easy-access method for addons depending on BlueMapCore:<br>
* <blockquote><pre>
* BmMap map = ((BlueMapMapImpl) blueMapMap).map();
* </pre></blockquote>
*/
public BmMap map() {
return unpack(map);
}
}

View File

@ -26,10 +26,12 @@ package de.bluecolored.bluemap.common.api;
import de.bluecolored.bluemap.api.BlueMapMap;
import de.bluecolored.bluemap.api.BlueMapWorld;
import de.bluecolored.bluemap.common.BlueMapService;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.file.Path;
import java.util.Collection;
@ -38,18 +40,16 @@ import java.util.stream.Collectors;
public class BlueMapWorldImpl implements BlueMapWorld {
private final WeakReference<Plugin> plugin;
private final String id;
private final WeakReference<World> world;
private final WeakReference<BlueMapService> blueMapService;
private final WeakReference<Plugin> plugin;
public BlueMapWorldImpl(Plugin plugin, World world) throws IOException {
this.plugin = new WeakReference<>(plugin);
this.id = plugin.getBlueMap().getWorldId(world.getSaveFolder());
public BlueMapWorldImpl(World world, BlueMapService blueMapService, @Nullable Plugin plugin) {
this.id = world.getId();
this.world = new WeakReference<>(world);
}
public World getWorld() {
return unpack(world);
this.blueMapService = new WeakReference<>(blueMapService);
this.plugin = new WeakReference<>(plugin);
}
@Override
@ -58,20 +58,52 @@ public class BlueMapWorldImpl implements BlueMapWorld {
}
@Override
@Deprecated
public Path getSaveFolder() {
return unpack(world).getSaveFolder();
World world = unpack(this.world);
if (world instanceof MCAWorld) {
return ((MCAWorld) world).getDimensionFolder();
} else {
throw new UnsupportedOperationException("This world-type has no save-folder.");
}
}
@Override
public Collection<BlueMapMap> getMaps() {
return unpack(plugin).getMaps().values().stream()
.filter(map -> map.getWorld().equals(unpack(world)))
.map(map -> new BlueMapMapImpl(unpack(plugin), map, this))
World world = unpack(this.world);
return unpack(blueMapService).getMaps().values().stream()
.filter(map -> map.getWorld().equals(world))
.map(map -> new BlueMapMapImpl(map, this, plugin.get()))
.collect(Collectors.toUnmodifiableSet());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlueMapWorldImpl that = (BlueMapWorldImpl) o;
return id.equals(that.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
private <T> T unpack(WeakReference<T> ref) {
return Objects.requireNonNull(ref.get(), "Reference lost to delegate object. Most likely BlueMap got reloaded and this instance is no longer valid.");
}
/**
* Easy-access method for addons depending on BlueMapCore:<br>
* <blockquote><pre>
* World world = ((BlueMapWorldImpl) blueMapWorld).world();
* </pre></blockquote>
*/
public World world() {
return unpack(world);
}
}

View File

@ -1,3 +1,27 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.api;

View File

@ -31,7 +31,6 @@ import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.common.rendermanager.MapPurgeTask;
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import java.io.IOException;
import java.util.Collection;
public class RenderManagerImpl implements RenderManager {
@ -49,19 +48,19 @@ public class RenderManagerImpl implements RenderManager {
@Override
public boolean scheduleMapUpdateTask(BlueMapMap map, boolean force) {
BlueMapMapImpl cmap = castMap(map);
return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.getBmMap(), force));
return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.map(), s -> force));
}
@Override
public boolean scheduleMapUpdateTask(BlueMapMap map, Collection<Vector2i> regions, boolean force) {
BlueMapMapImpl cmap = castMap(map);
return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.getBmMap(), regions, force));
return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.map(), regions, s -> force));
}
@Override
public boolean scheduleMapPurgeTask(BlueMapMap map) throws IOException {
public boolean scheduleMapPurgeTask(BlueMapMap map) {
BlueMapMapImpl cmap = castMap(map);
return renderManager.scheduleRenderTask(new MapPurgeTask(cmap.getBmMap()));
return renderManager.scheduleRenderTask(new MapPurgeTask(cmap.map()));
}
@Override
@ -82,7 +81,7 @@ public class RenderManagerImpl implements RenderManager {
@Override
public void start() {
if (!isRunning()){
renderManager.start(plugin.getConfigs().getCoreConfig().getRenderThreadCount());
renderManager.start(plugin.getBlueMap().getConfig().getCoreConfig().getRenderThreadCount());
}
plugin.getPluginState().setRenderThreadsEnabled(true);
}

View File

@ -25,35 +25,47 @@
package de.bluecolored.bluemap.common.api;
import de.bluecolored.bluemap.api.WebApp;
import de.bluecolored.bluemap.common.BlueMapService;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.util.FileHelper;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import java.util.stream.Stream;
public class WebAppImpl implements WebApp {
private static final Path IMAGE_ROOT_PATH = Path.of("data", "images");
private final Plugin plugin;
private final BlueMapService blueMapService;
private final @Nullable Plugin plugin;
private final Timer timer = new Timer("BlueMap-WebbAppImpl-Timer", true);
private @Nullable TimerTask scheduledWebAppSettingsUpdate;
public WebAppImpl(BlueMapService blueMapService, @Nullable Plugin plugin) {
this.blueMapService = blueMapService;
this.plugin = plugin;
}
public WebAppImpl(Plugin plugin) {
this.blueMapService = plugin.getBlueMap();
this.plugin = plugin;
}
@Override
public Path getWebRoot() {
return plugin.getConfigs().getWebappConfig().getWebroot();
return blueMapService.getConfig().getWebappConfig().getWebroot();
}
@Override
public void setPlayerVisibility(UUID player, boolean visible) {
if (plugin == null) return; // fail silently: not supported on non-plugin platforms
if (visible) {
plugin.getPluginState().removeHiddenPlayer(player);
} else {
@ -63,29 +75,61 @@ public class WebAppImpl implements WebApp {
@Override
public boolean getPlayerVisibility(UUID player) {
if (plugin == null) return false; // fail silently: not supported on non-plugin platforms
return !plugin.getPluginState().isPlayerHidden(player);
}
@Override
public void registerScript(String url) {
plugin.getBlueMap().getWebFilesManager().getScripts().add(url);
public synchronized void registerScript(String url) {
Logger.global.logDebug("Registering script from API: " + url);
blueMapService.getWebFilesManager().getScripts().add(url);
scheduleUpdateWebAppSettings();
}
@Override
public void registerStyle(String url) {
plugin.getBlueMap().getWebFilesManager().getStyles().add(url);
public synchronized void registerStyle(String url) {
Logger.global.logDebug("Registering style from API: " + url);
blueMapService.getWebFilesManager().getStyles().add(url);
scheduleUpdateWebAppSettings();
}
/**
* Save webapp-settings after a short delay, if no other save is already scheduled.
* (to bulk-save changes in case there is a lot of scripts being registered at once)
*/
private synchronized void scheduleUpdateWebAppSettings() {
if (!blueMapService.getConfig().getWebappConfig().isEnabled()) return;
if (scheduledWebAppSettingsUpdate != null) return;
timer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (WebAppImpl.this) {
try {
if (blueMapService.getConfig().getWebappConfig().isEnabled())
blueMapService.getWebFilesManager().saveSettings();
} catch (IOException ex) {
Logger.global.logError("Failed to update webapp settings", ex);
} finally {
scheduledWebAppSettingsUpdate = null;
}
}
}
}, 1000);
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public String createImage(BufferedImage image, String path) throws IOException {
path = path.replaceAll("[^a-zA-Z0-9_.\\-/]", "_");
Path webRoot = getWebRoot().toAbsolutePath();
String separator = webRoot.getFileSystem().getSeparator();
Path imageRootFolder = webRoot.resolve(IMAGE_ROOT_PATH);
Path imagePath = imageRootFolder.resolve(Path.of(path.replace("/", separator) + ".png")).toAbsolutePath();
Path imageRootFolder = webRoot.resolve("data").resolve("images");
Path imagePath = imageRootFolder.resolve(path.replace("/", separator) + ".png").toAbsolutePath();
FileHelper.createDirectories(imagePath.getParent());
Files.deleteIfExists(imagePath);
@ -98,11 +142,13 @@ public class WebAppImpl implements WebApp {
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public Map<String, String> availableImages() throws IOException {
Path webRoot = getWebRoot().toAbsolutePath();
String separator = webRoot.getFileSystem().getSeparator();
Path imageRootPath = webRoot.resolve("data").resolve(IMAGE_ROOT_PATH).toAbsolutePath();
Path imageRootPath = webRoot.resolve("data").resolve("images").toAbsolutePath();
Map<String, String> availableImagesMap = new HashMap<>();

View File

@ -24,31 +24,40 @@
*/
package de.bluecolored.bluemap.common.config;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.BlueMapConfigProvider;
import de.bluecolored.bluemap.common.BlueMapConfiguration;
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.pack.datapack.DataPack;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.util.Key;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.*;
import java.util.stream.Stream;
@DebugDump
public class BlueMapConfigs implements BlueMapConfigProvider {
@Getter
public class BlueMapConfigManager implements BlueMapConfiguration {
public static final String CORE_CONFIG_NAME = "core";
public static final String WEBSERVER_CONFIG_NAME = "webserver";
public static final String WEBAPP_CONFIG_NAME = "webapp";
public static final String PLUGIN_CONFIG_NAME = "plugin";
public static final String MAPS_CONFIG_FOLDER_NAME = "maps";
public static final String STORAGES_CONFIG_FOLDER_NAME = "storages";
public static final String MAP_STORAGE_CONFIG_NAME = MAPS_CONFIG_FOLDER_NAME + "/map";
public static final String FILE_STORAGE_CONFIG_NAME = STORAGES_CONFIG_FOLDER_NAME + "/file";
public static final String SQL_STORAGE_CONFIG_NAME = STORAGES_CONFIG_FOLDER_NAME + "/sql";
private final ServerInterface serverInterface;
private final ConfigManager configManager;
private final CoreConfig coreConfig;
@ -57,85 +66,61 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
private final PluginConfig pluginConfig;
private final Map<String, MapConfig> mapConfigs;
private final Map<String, StorageConfig> storageConfigs;
private final Path packsFolder;
private final @Nullable String minecraftVersion;
private final @Nullable Path modsFolder;
public BlueMapConfigs(ServerInterface serverInterface) throws ConfigurationException {
this(serverInterface, Path.of("bluemap"), Path.of("bluemap", "web"), true);
}
@Builder
private BlueMapConfigManager(
@NonNull Path configRoot,
@Nullable String minecraftVersion,
@Nullable Path defaultDataFolder,
@Nullable Path defaultWebroot,
@Nullable Collection<ServerWorld> autoConfigWorlds,
@Nullable Boolean usePluginConfig,
@Nullable Boolean useMetricsConfig,
@Nullable Path packsFolder,
@Nullable Path modsFolder
) throws ConfigurationException {
// set defaults
if (defaultDataFolder == null) defaultDataFolder = Path.of("bluemap");
if (defaultWebroot == null) defaultWebroot = Path.of("bluemap", "web");
if (autoConfigWorlds == null) autoConfigWorlds = Collections.emptyList();
if (usePluginConfig == null) usePluginConfig = true;
if (useMetricsConfig == null) useMetricsConfig = true;
if (packsFolder == null) packsFolder = configRoot.resolve("packs");
public BlueMapConfigs(ServerInterface serverInterface, Path defaultDataFolder, Path defaultWebroot, boolean usePluginConf) throws ConfigurationException {
this.serverInterface = serverInterface;
this.configManager = new ConfigManager(serverInterface.getConfigFolder());
this.coreConfig = loadCoreConfig(defaultDataFolder);
// load
this.configManager = new ConfigManager(configRoot);
this.coreConfig = loadCoreConfig(defaultDataFolder, useMetricsConfig);
this.webappConfig = loadWebappConfig(defaultWebroot);
this.webserverConfig = loadWebserverConfig(webappConfig.getWebroot());
this.pluginConfig = usePluginConf ? loadPluginConfig() : new PluginConfig();
this.webserverConfig = loadWebserverConfig(webappConfig.getWebroot(), coreConfig.getData());
this.pluginConfig = usePluginConfig ? loadPluginConfig() : new PluginConfig();
this.storageConfigs = Collections.unmodifiableMap(loadStorageConfigs(webappConfig.getWebroot()));
this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs());
this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs(autoConfigWorlds));
this.packsFolder = packsFolder;
this.minecraftVersion = minecraftVersion;
this.modsFolder = modsFolder;
}
public ConfigManager getConfigManager() {
return configManager;
}
@Override
public CoreConfig getCoreConfig() {
return coreConfig;
}
@Override
public WebappConfig getWebappConfig() {
return webappConfig;
}
@Override
public WebserverConfig getWebserverConfig() {
return webserverConfig;
}
@Override
public PluginConfig getPluginConfig() {
return pluginConfig;
}
@Override
public Map<String, MapConfig> getMapConfigs() {
return mapConfigs;
}
@Override
public Map<String, StorageConfig> getStorageConfigs() {
return storageConfigs;
}
private synchronized CoreConfig loadCoreConfig(Path defaultDataFolder) throws ConfigurationException {
Path configFileRaw = Path.of("core");
Path configFile = configManager.findConfigPath(configFileRaw);
private CoreConfig loadCoreConfig(Path defaultDataFolder, boolean useMetricsConfig) throws ConfigurationException {
Path configFile = configManager.resolveConfigFile(CORE_CONFIG_NAME);
Path configFolder = configFile.getParent();
if (!Files.exists(configFile)) {
// determine render-thread preset (very pessimistic, rather let people increase it themselves)
Runtime runtime = Runtime.getRuntime();
int availableCores = runtime.availableProcessors();
long availableMemoryMiB = runtime.maxMemory() / 1024L / 1024L;
int presetRenderThreadCount = 1;
if (availableCores >= 6 && availableMemoryMiB >= 4096)
presetRenderThreadCount = 2;
if (availableCores >= 10 && availableMemoryMiB >= 8192)
presetRenderThreadCount = 3;
try {
FileHelper.createDirectories(configFolder);
Files.writeString(
configFolder.resolve("core.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/core.conf")
.setConditional("metrics", serverInterface.isMetricsEnabled() == Tristate.UNDEFINED)
configFile,
configManager.loadConfigTemplate(CORE_CONFIG_NAME)
.setConditional("metrics", useMetricsConfig)
.setVariable("timestamp", LocalDateTime.now().withNano(0).toString())
.setVariable("version", BlueMap.VERSION)
.setVariable("data", formatPath(defaultDataFolder))
.setVariable("implementation", "bukkit")
.setVariable("render-thread-count", Integer.toString(presetRenderThreadCount))
.setVariable("render-thread-count", Integer.toString(suggestRenderThreadCount()))
.setVariable("logfile", formatPath(defaultDataFolder.resolve("logs").resolve("debug.log")))
.setVariable("logfile-with-time", formatPath(defaultDataFolder.resolve("logs").resolve("debug_%1$tF_%1$tT.log")))
.build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
@ -144,21 +129,37 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
}
}
return configManager.loadConfig(configFileRaw, CoreConfig.class);
return configManager.loadConfig(CORE_CONFIG_NAME, CoreConfig.class);
}
private synchronized WebserverConfig loadWebserverConfig(Path defaultWebroot) throws ConfigurationException {
Path configFileRaw = Path.of("webserver");
Path configFile = configManager.findConfigPath(configFileRaw);
/**
* determine render-thread preset (very pessimistic, rather let people increase it themselves)
*/
private int suggestRenderThreadCount() {
Runtime runtime = Runtime.getRuntime();
int availableCores = runtime.availableProcessors();
long availableMemoryMiB = runtime.maxMemory() / 1024L / 1024L;
int presetRenderThreadCount = 1;
if (availableCores >= 6 && availableMemoryMiB >= 4096)
presetRenderThreadCount = 2;
if (availableCores >= 10 && availableMemoryMiB >= 8192)
presetRenderThreadCount = 3;
return presetRenderThreadCount;
}
private WebserverConfig loadWebserverConfig(Path defaultWebroot, Path dataRoot) throws ConfigurationException {
Path configFile = configManager.resolveConfigFile(WEBSERVER_CONFIG_NAME);
Path configFolder = configFile.getParent();
if (!Files.exists(configFile)) {
try {
FileHelper.createDirectories(configFolder);
Files.writeString(
configFolder.resolve("webserver.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/webserver.conf")
configFile,
configManager.loadConfigTemplate(WEBSERVER_CONFIG_NAME)
.setVariable("webroot", formatPath(defaultWebroot))
.setVariable("logfile", formatPath(dataRoot.resolve("logs").resolve("webserver.log")))
.setVariable("logfile-with-time", formatPath(dataRoot.resolve("logs").resolve("webserver_%1$tF_%1$tT.log")))
.build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
@ -167,20 +168,19 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
}
}
return configManager.loadConfig(configFileRaw, WebserverConfig.class);
return configManager.loadConfig(WEBSERVER_CONFIG_NAME, WebserverConfig.class);
}
private synchronized WebappConfig loadWebappConfig(Path defaultWebroot) throws ConfigurationException {
Path configFileRaw = Path.of("webapp");
Path configFile = configManager.findConfigPath(configFileRaw);
private WebappConfig loadWebappConfig(Path defaultWebroot) throws ConfigurationException {
Path configFile = configManager.resolveConfigFile(WEBAPP_CONFIG_NAME);
Path configFolder = configFile.getParent();
if (!Files.exists(configFile)) {
try {
FileHelper.createDirectories(configFolder);
Files.writeString(
configFolder.resolve("webapp.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/webapp.conf")
configFile,
configManager.loadConfigTemplate(WEBAPP_CONFIG_NAME)
.setVariable("webroot", formatPath(defaultWebroot))
.build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
@ -190,20 +190,19 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
}
}
return configManager.loadConfig(configFileRaw, WebappConfig.class);
return configManager.loadConfig(WEBAPP_CONFIG_NAME, WebappConfig.class);
}
private synchronized PluginConfig loadPluginConfig() throws ConfigurationException {
Path configFileRaw = Path.of("plugin");
Path configFile = configManager.findConfigPath(configFileRaw);
private PluginConfig loadPluginConfig() throws ConfigurationException {
Path configFile = configManager.resolveConfigFile(PLUGIN_CONFIG_NAME);
Path configFolder = configFile.getParent();
if (!Files.exists(configFile)) {
try {
FileHelper.createDirectories(configFolder);
Files.writeString(
configFolder.resolve("plugin.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/plugin.conf")
configFile,
configManager.loadConfigTemplate(PLUGIN_CONFIG_NAME)
.build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
@ -212,54 +211,73 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
}
}
return configManager.loadConfig(configFileRaw, PluginConfig.class);
return configManager.loadConfig(PLUGIN_CONFIG_NAME, PluginConfig.class);
}
private synchronized Map<String, MapConfig> loadMapConfigs() throws ConfigurationException {
private Map<String, MapConfig> loadMapConfigs(Collection<ServerWorld> autoConfigWorlds) throws ConfigurationException {
Map<String, MapConfig> mapConfigs = new HashMap<>();
Path mapFolder = Paths.get("maps");
Path mapConfigFolder = configManager.getConfigRoot().resolve(mapFolder);
Path mapConfigFolder = configManager.getConfigRoot().resolve(MAPS_CONFIG_FOLDER_NAME);
if (!Files.exists(mapConfigFolder)){
try {
FileHelper.createDirectories(mapConfigFolder);
var worlds = serverInterface.getLoadedWorlds();
if (worlds.isEmpty()) {
if (autoConfigWorlds.isEmpty()) {
Path worldFolder = Path.of("world");
Files.writeString(
mapConfigFolder.resolve("overworld.conf"),
createOverworldMapTemplate("Overworld", Path.of("world"), 0).build(),
configManager.resolveConfigFile(MAPS_CONFIG_FOLDER_NAME + "/overworld"),
createOverworldMapTemplate("Overworld", worldFolder,
DataPack.DIMENSION_OVERWORLD, 0).build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
Files.writeString(
mapConfigFolder.resolve("nether.conf"),
createNetherMapTemplate("Nether", Path.of("world", "DIM-1"), 0).build(),
configManager.resolveConfigFile(MAPS_CONFIG_FOLDER_NAME + "/nether"),
createNetherMapTemplate("Nether", worldFolder,
DataPack.DIMENSION_THE_NETHER, 0).build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
Files.writeString(
mapConfigFolder.resolve("end.conf"),
createEndMapTemplate("End", Path.of("world", "DIM1"), 0).build(),
configManager.resolveConfigFile(MAPS_CONFIG_FOLDER_NAME + "/end"),
createEndMapTemplate("End", worldFolder,
DataPack.DIMENSION_THE_END, 0).build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
} else {
for (var world : worlds) {
String name = world.getName().orElse(world.getDimension().getName());
Path worldFolder = world.getSaveFolder();
// make sure overworld-dimensions come first, so they are the ones where the
// dimension-key is omitted in the generated map-id
List<ServerWorld> overworldFirstAutoConfigWorlds = new ArrayList<>(autoConfigWorlds.size());
overworldFirstAutoConfigWorlds.addAll(autoConfigWorlds);
overworldFirstAutoConfigWorlds.sort(Comparator.comparingInt(w ->
DataPack.DIMENSION_OVERWORLD.equals(w.getDimension()) ? 0 : 1
));
Path configFile = mapConfigFolder.resolve(sanitiseMapId(name.toLowerCase(Locale.ROOT)) + ".conf");
Set<String> mapIds = new HashSet<>();
for (var world : overworldFirstAutoConfigWorlds) {
Path worldFolder = world.getWorldFolder().normalize();
Key dimension = world.getDimension();
String dimensionName = dimension.getNamespace().equals("minecraft") ?
dimension.getValue() : dimension.getFormatted();
// find unique map id
String id = sanitiseMapId(worldFolder.getFileName().toString()).toLowerCase(Locale.ROOT);
if (mapIds.contains(id))
id = sanitiseMapId(worldFolder.getFileName() + "_" + dimensionName).toLowerCase(Locale.ROOT);
int i = 1;
while (Files.exists(configFile)) {
configFile = mapConfigFolder.resolve(sanitiseMapId(name.toLowerCase(Locale.ROOT)) + '_' + (++i) + ".conf");
}
String uniqueId = id;
while (mapIds.contains(uniqueId))
uniqueId = id + "_" + (++i);
mapIds.add(uniqueId);
if (i > 1) name = name + " " + i;
Path configFile = configManager.resolveConfigFile(MAPS_CONFIG_FOLDER_NAME + "/" + uniqueId);
String name = worldFolder.getFileName() + " (" + dimensionName + ")";
if (i > 1) name = name + " (" + i + ")";
ConfigTemplate template;
switch (world.getDimension()) {
case NETHER: template = createNetherMapTemplate(name, worldFolder, i - 1); break;
case END: template = createEndMapTemplate(name, worldFolder, i - 1); break;
default: template = createOverworldMapTemplate(name, worldFolder, i - 1); break;
}
ConfigTemplate template = switch (world.getDimension().getFormatted()) {
case "minecraft:the_nether" -> createNetherMapTemplate(name, worldFolder, dimension, i - 1);
case "minecraft:the_end" -> createEndMapTemplate(name, worldFolder, dimension, i - 1);
default -> createOverworldMapTemplate(name, worldFolder, dimension, i - 1);
};
Files.writeString(
configFile,
@ -279,8 +297,7 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
try (Stream<Path> configFiles = Files.list(mapConfigFolder)) {
for (var configFile : configFiles.toArray(Path[]::new)) {
if (!configManager.isConfigFile(configFile)) continue;
Path rawConfig = configManager.getRaw(configFile);
String id = sanitiseMapId(rawConfig.getFileName().toString());
String id = sanitiseMapId(configManager.getConfigName(configFile));
if (mapConfigs.containsKey(id)) {
throw new ConfigurationException("At least two of your map-config file-names result in ambiguous map-id's!\n" +
@ -288,7 +305,7 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
"To resolve this issue, rename this file to something else.");
}
MapConfig mapConfig = configManager.loadConfig(rawConfig, MapConfig.class);
MapConfig mapConfig = configManager.loadConfig(configFile, MapConfig.class);
mapConfigs.put(id, mapConfig);
}
} catch (IOException ex) {
@ -301,25 +318,24 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
return mapConfigs;
}
private synchronized Map<String, StorageConfig> loadStorageConfigs(Path defaultWebroot) throws ConfigurationException {
private Map<String, StorageConfig> loadStorageConfigs(Path defaultWebroot) throws ConfigurationException {
Map<String, StorageConfig> storageConfigs = new HashMap<>();
Path storageFolder = Paths.get("storages");
Path storageConfigFolder = configManager.getConfigRoot().resolve(storageFolder);
Path storageConfigFolder = configManager.getConfigRoot().resolve(STORAGES_CONFIG_FOLDER_NAME);
if (!Files.exists(storageConfigFolder)){
try {
FileHelper.createDirectories(storageConfigFolder);
Files.writeString(
storageConfigFolder.resolve("file.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/storages/file.conf")
configManager.resolveConfigFile(FILE_STORAGE_CONFIG_NAME),
configManager.loadConfigTemplate(FILE_STORAGE_CONFIG_NAME)
.setVariable("root", formatPath(defaultWebroot.resolve("maps")))
.build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
Files.writeString(
storageConfigFolder.resolve("sql.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/storages/sql.conf").build(),
configManager.resolveConfigFile(SQL_STORAGE_CONFIG_NAME),
configManager.loadConfigTemplate(SQL_STORAGE_CONFIG_NAME).build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
} catch (IOException | NullPointerException ex) {
@ -334,11 +350,10 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
try (Stream<Path> configFiles = Files.list(storageConfigFolder)) {
for (var configFile : configFiles.toArray(Path[]::new)) {
if (!configManager.isConfigFile(configFile)) continue;
Path rawConfig = configManager.getRaw(configFile);
String id = rawConfig.getFileName().toString();
String id = configManager.getConfigName(configFile);
StorageConfig storageConfig = configManager.loadConfig(rawConfig, StorageConfig.class); // load superclass
storageConfig = configManager.loadConfig(rawConfig, storageConfig.getStorageType().getConfigType()); // load actual config type
StorageConfig storageConfig = configManager.loadConfig(configFile, StorageConfig.Base.class); // load superclass
storageConfig = configManager.loadConfig(configFile, storageConfig.getStorageType().getConfigType()); // load actual config type
storageConfigs.put(id, storageConfig);
}
@ -356,52 +371,71 @@ public class BlueMapConfigs implements BlueMapConfigProvider {
return id.replaceAll("\\W", "_");
}
private ConfigTemplate createOverworldMapTemplate(String name, Path worldFolder, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
private ConfigTemplate createOverworldMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate(MAP_STORAGE_CONFIG_NAME)
.setVariable("name", name)
.setVariable("sorting", "" + index)
.setVariable("world", formatPath(worldFolder))
.setVariable("dimension", dimension.getFormatted())
.setVariable("sky-color", "#7dabff")
.setVariable("void-color", "#000000")
.setVariable("ambient-light", "0.1")
.setVariable("world-sky-light", "15")
.setVariable("remove-caves-below-y", "55")
.setConditional("max-y-comment", true)
.setVariable("max-y", "100");
}
private ConfigTemplate createNetherMapTemplate(String name, Path worldFolder, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
private ConfigTemplate createNetherMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate(MAP_STORAGE_CONFIG_NAME)
.setVariable("name", name)
.setVariable("sorting", "" + (100 + index))
.setVariable("world", formatPath(worldFolder))
.setVariable("dimension", dimension.getFormatted())
.setVariable("sky-color", "#290000")
.setVariable("void-color", "#150000")
.setVariable("ambient-light", "0.6")
.setVariable("world-sky-light", "0")
.setVariable("remove-caves-below-y", "-10000")
.setConditional("max-y-comment", false)
.setVariable("max-y", "90");
}
private ConfigTemplate createEndMapTemplate(String name, Path worldFolder, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
private ConfigTemplate createEndMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate(MAP_STORAGE_CONFIG_NAME)
.setVariable("name", name)
.setVariable("sorting", "" + (200 + index))
.setVariable("world", formatPath(worldFolder))
.setVariable("dimension", dimension.getFormatted())
.setVariable("sky-color", "#080010")
.setVariable("void-color", "#080010")
.setVariable("ambient-light", "0.6")
.setVariable("world-sky-light", "0")
.setVariable("remove-caves-below-y", "-10000")
.setConditional("max-y-comment", true)
.setVariable("max-y", "100");
}
private String formatPath(Path path) {
return Path.of("")
// normalize path
path = Path.of("")
.toAbsolutePath()
.relativize(path.toAbsolutePath())
.normalize()
.toString()
.replace("\\", "\\\\");
.normalize();
String pathString = path.toString();
String formatted = pathString;
String separator = FileSystems.getDefault().getSeparator();
// try to replace separator with standardized forward slash
if (!separator.equals("/"))
formatted = pathString.replace(separator, "/");
// sanity check forward slash compatibility
if (!Path.of(formatted).equals(path))
formatted = pathString;
// escape all backslashes
formatted = formatted.replace("\\", "\\\\");
return formatted;
}
}

View File

@ -0,0 +1,69 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.config;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
import java.util.function.Supplier;
public interface ConfigLoader extends Keyed {
ConfigLoader HOCON = new Impl(Key.bluemap("hocon"), ".conf", HoconConfigurationLoader::builder);
ConfigLoader JSON = new Impl(Key.bluemap("json"), ".json", GsonConfigurationLoader::builder);
ConfigLoader DEFAULT = HOCON;
Registry<ConfigLoader> REGISTRY = new Registry<>(
HOCON,
JSON
);
String getFileSuffix();
AbstractConfigurationLoader.Builder<?, ?> createLoaderBuilder();
@RequiredArgsConstructor
@Getter
class Impl implements ConfigLoader {
private final Key key;
private final String fileSuffix;
private final Supplier<AbstractConfigurationLoader.Builder<?, ?>> builderSupplier;
@Override
public AbstractConfigurationLoader.Builder<?, ?> createLoaderBuilder() {
return builderSupplier.get();
}
}
}

View File

@ -25,30 +25,28 @@
package de.bluecolored.bluemap.common.config;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.common.config.typeserializer.KeyTypeSerializer;
import de.bluecolored.bluemap.common.config.typeserializer.Vector2iTypeSerializer;
import de.bluecolored.bluemap.core.BlueMap;
import org.apache.commons.io.IOUtils;
import de.bluecolored.bluemap.core.util.Key;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
import org.spongepowered.configurate.loader.ConfigurationLoader;
import org.spongepowered.configurate.serialize.SerializationException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.Objects;
public class ConfigManager {
private static final String[] CONFIG_FILE_ENDINGS = new String[] {
".conf",
".json"
};
private static final String CONFIG_TEMPLATE_RESOURCE_PATH = "/de/bluecolored/bluemap/config/";
private final Path configRoot;
@ -56,30 +54,66 @@ public class ConfigManager {
this.configRoot = configRoot;
}
public <T> T loadConfig(Path rawPath, Class<T> type) throws ConfigurationException {
Path path = findConfigPath(rawPath);
ConfigurationNode configNode = loadConfigFile(path);
public <T> T loadConfig(String name, Class<T> type) throws ConfigurationException {
Path file = resolveConfigFile(name);
return loadConfig(file, type);
}
public <T> T loadConfig(Path file, Class<T> type) throws ConfigurationException {
ConfigurationNode configNode = loadConfigFile(file);
try {
return Objects.requireNonNull(configNode.get(type));
} catch (SerializationException | NullPointerException ex) {
throw new ConfigurationException(
"BlueMap failed to parse this file:\n" +
path + "\n" +
file + "\n" +
"Check if the file is correctly formatted and all values are correct!",
ex);
}
}
public ConfigurationNode loadConfig(Path rawPath) throws ConfigurationException {
Path path = findConfigPath(rawPath);
return loadConfigFile(path);
public ConfigTemplate loadConfigTemplate(String name) throws IOException {
String resource = CONFIG_TEMPLATE_RESOURCE_PATH + name + ConfigLoader.DEFAULT.getFileSuffix();
try (InputStream in = BlueMap.class.getResourceAsStream(resource)) {
if (in == null) throw new IOException("Resource not found: " + resource);
StringWriter writer = new StringWriter();
InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
reader.transferTo(writer);
return new ConfigTemplate(writer.toString());
}
}
public ConfigTemplate loadConfigTemplate(String resource) throws IOException {
InputStream in = BlueMap.class.getResourceAsStream(resource);
if (in == null) throw new IOException("Resource not found: " + resource);
String configTemplate = IOUtils.toString(in, StandardCharsets.UTF_8);
return new ConfigTemplate(configTemplate);
public Path resolveConfigFile(String name) {
for (ConfigLoader configLoader : ConfigLoader.REGISTRY.values()) {
Path path = configRoot.resolve(name + configLoader.getFileSuffix());
if (Files.isRegularFile(path)) return path;
}
return configRoot.resolve(name + ConfigLoader.DEFAULT.getFileSuffix());
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isConfigFile(Path file) {
String fileName = file.getFileName().toString();
for (ConfigLoader configLoader : ConfigLoader.REGISTRY.values())
if (fileName.endsWith(configLoader.getFileSuffix())) return true;
return false;
}
public String getConfigName(Path file) {
String fileName = file.getFileName().toString();
for (ConfigLoader configLoader : ConfigLoader.REGISTRY.values()) {
String suffix = configLoader.getFileSuffix();
if (fileName.endsWith(suffix))
return fileName.substring(0, fileName.length() - suffix.length());
}
return fileName;
}
public Path getConfigRoot() {
return configRoot;
}
private ConfigurationNode loadConfigFile(Path path) throws ConfigurationException {
@ -108,63 +142,23 @@ public class ConfigManager {
}
}
public Path getConfigRoot() {
return configRoot;
}
public Path findConfigPath(Path rawPath) {
if (!rawPath.startsWith(configRoot))
rawPath = configRoot.resolve(rawPath);
for (String fileEnding : CONFIG_FILE_ENDINGS) {
if (rawPath.getFileName().endsWith(fileEnding)) return rawPath;
}
for (String fileEnding : CONFIG_FILE_ENDINGS) {
Path path = rawPath.getParent().resolve(rawPath.getFileName() + fileEnding);
if (Files.exists(path)) return path;
}
return rawPath.getParent().resolve(rawPath.getFileName() + CONFIG_FILE_ENDINGS[0]);
}
public boolean isConfigFile(Path path) {
if (!Files.isRegularFile(path)) return false;
String fileName = path.getFileName().toString();
for (String fileEnding : CONFIG_FILE_ENDINGS) {
if (fileName.endsWith(fileEnding)) return true;
}
return false;
}
public Path getRaw(Path path) {
String fileName = path.getFileName().toString();
String rawName = null;
for (String fileEnding : CONFIG_FILE_ENDINGS) {
if (fileName.endsWith(fileEnding)) {
rawName = fileName.substring(0, fileName.length() - fileEnding.length());
private ConfigurationLoader<? extends ConfigurationNode> getLoader(Path path){
AbstractConfigurationLoader.Builder<?, ?> builder = null;
for (ConfigLoader loader : ConfigLoader.REGISTRY.values()) {
if (path.getFileName().endsWith(loader.getFileSuffix())) {
builder = loader.createLoaderBuilder();
break;
}
}
if (rawName == null) return path;
return path.getParent().resolve(rawName);
}
private ConfigurationLoader<? extends ConfigurationNode> getLoader(Path path){
AbstractConfigurationLoader.Builder<?, ?> builder;
if (path.getFileName().endsWith(".json"))
builder = GsonConfigurationLoader.builder();
else
builder = HoconConfigurationLoader.builder();
if (builder == null)
builder = ConfigLoader.DEFAULT.createLoaderBuilder();
return builder
.path(path)
.defaultOptions(o -> o.serializers(b -> {
b.register(Vector2i.class, new Vector2iTypeSerializer());
b.register(Key.class, new KeyTypeSerializer());
}))
.build();
}

View File

@ -24,13 +24,11 @@
*/
package de.bluecolored.bluemap.common.config;
import de.bluecolored.bluemap.api.debug.DebugDump;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.nio.file.Path;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
public class CoreConfig {
@ -44,6 +42,8 @@ public class CoreConfig {
private boolean scanForModResources = true;
private LogConfig log = new LogConfig();
public boolean isAcceptDownload() {
return acceptDownload;
}
@ -69,4 +69,24 @@ public class CoreConfig {
return scanForModResources;
}
public LogConfig getLog() {
return log;
}
@ConfigSerializable
public static class LogConfig {
private String file = null;
private boolean append = false;
public String getFile() {
return file;
}
public boolean isAppend() {
return append;
}
}
}

View File

@ -26,44 +26,45 @@ package de.bluecolored.bluemap.common.config;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.map.MapSettings;
import de.bluecolored.bluemap.core.util.Key;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.nio.file.Path;
import java.util.Optional;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
@Getter
public class MapConfig implements MapSettings {
private String name = null;
@Nullable private Path world = null;
@Nullable private Key dimension = null;
private Path world = null;
@Nullable private String name = null;
private int sorting = 0;
private Vector2i startPos = null;
@Nullable private Vector2i startPos = null;
private String skyColor = "#7dabff";
private String voidColor = "#000000";
private float ambientLight = 0;
private int worldSkyLight = 15;
private int removeCavesBelowY = 55;
private int caveDetectionOceanFloor = 10000;
private boolean caveDetectionUsesBlockLight = false;
private int minX = Integer.MIN_VALUE;
private int maxX = Integer.MAX_VALUE;
private int minZ = Integer.MIN_VALUE;
private int maxZ = Integer.MAX_VALUE;
private int minY = Integer.MIN_VALUE;
private int maxY = Integer.MAX_VALUE;
@Getter(AccessLevel.NONE) private int minX = Integer.MIN_VALUE;
@Getter(AccessLevel.NONE) private int maxX = Integer.MAX_VALUE;
@Getter(AccessLevel.NONE) private int minZ = Integer.MIN_VALUE;
@Getter(AccessLevel.NONE) private int maxZ = Integer.MAX_VALUE;
@Getter(AccessLevel.NONE) private int minY = Integer.MIN_VALUE;
@Getter(AccessLevel.NONE) private int maxY = Integer.MAX_VALUE;
private transient Vector3i min = null;
private transient Vector3i max = null;
@ -79,7 +80,7 @@ public class MapConfig implements MapSettings {
private boolean ignoreMissingLightData = false;
private ConfigurationNode markerSets = null;
@Nullable private ConfigurationNode markerSets = null;
// hidden config fields
private int hiresTileSize = 32;
@ -87,56 +88,6 @@ public class MapConfig implements MapSettings {
private int lodCount = 3;
private int lodFactor = 5;
@Nullable
public String getName() {
return name;
}
@Nullable
public Path getWorld() {
return world;
}
@Override
public int getSorting() {
return sorting;
}
@Override
public Optional<Vector2i> getStartPos() {
return Optional.ofNullable(startPos);
}
@Override
public String getSkyColor() {
return skyColor;
}
@Override
public float getAmbientLight() {
return ambientLight;
}
@Override
public int getWorldSkyLight() {
return worldSkyLight;
}
@Override
public int getRemoveCavesBelowY() {
return removeCavesBelowY;
}
@Override
public boolean isCaveDetectionUsesBlockLight() {
return caveDetectionUsesBlockLight;
}
@Override
public int getCaveDetectionOceanFloor() {
return caveDetectionOceanFloor;
}
public Vector3i getMinPos() {
if (min == null) min = new Vector3i(minX, minY, minZ);
return min;
@ -147,57 +98,4 @@ public class MapConfig implements MapSettings {
return max;
}
@Override
public long getMinInhabitedTime() {
return minInhabitedTime;
}
@Override
public int getMinInhabitedTimeRadius() {
return minInhabitedTimeRadius;
}
@Override
public boolean isRenderEdges() {
return renderEdges;
}
@Override
public boolean isSaveHiresLayer() {
return saveHiresLayer;
}
public String getStorage() {
return storage;
}
public boolean isIgnoreMissingLightData() {
return ignoreMissingLightData;
}
@Nullable
public ConfigurationNode getMarkerSets() {
return markerSets;
}
@Override
public int getHiresTileSize() {
return hiresTileSize;
}
@Override
public int getLowresTileSize() {
return lowresTileSize;
}
@Override
public int getLodCount() {
return lodCount;
}
@Override
public int getLodFactor() {
return lodFactor;
}
}

View File

@ -24,14 +24,12 @@
*/
package de.bluecolored.bluemap.common.config;
import de.bluecolored.bluemap.api.debug.DebugDump;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
public class PluginConfig {

View File

@ -24,7 +24,6 @@
*/
package de.bluecolored.bluemap.common.config;
import de.bluecolored.bluemap.api.debug.DebugDump;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.nio.file.Path;
@ -33,7 +32,6 @@ import java.util.Optional;
import java.util.Set;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
public class WebappConfig {
@ -45,6 +43,7 @@ public class WebappConfig {
private boolean useCookies = true;
private boolean enableFreeFlight = true;
private boolean defaultToFlatView = false;
private String startLocation = null;
@ -83,6 +82,10 @@ public class WebappConfig {
return enableFreeFlight;
}
public boolean isDefaultToFlatView() {
return defaultToFlatView;
}
public Optional<String> getStartLocation() {
return Optional.ofNullable(startLocation);
}

View File

@ -24,7 +24,6 @@
*/
package de.bluecolored.bluemap.common.config;
import de.bluecolored.bluemap.api.debug.DebugDump;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.net.InetAddress;
@ -33,18 +32,17 @@ import java.net.UnknownHostException;
import java.nio.file.Path;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
public class WebserverConfig {
private boolean enabled = true;
private Path webroot = Path.of("bluemap", "web");
private String ip = "0.0.0.0";
private int port = 8100;
private LogConfig log = new LogConfig();
public boolean isEnabled() {
return enabled;
}
@ -71,4 +69,29 @@ public class WebserverConfig {
return port;
}
public LogConfig getLog() {
return log;
}
@ConfigSerializable
public static class LogConfig {
private String file = null;
private boolean append = false;
private String format = "%1$s \"%3$s %4$s %5$s\" %6$s %7$s";
public String getFile() {
return file;
}
public boolean isAppend() {
return append;
}
public String getFormat() {
return format;
}
}
}

View File

@ -0,0 +1,78 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.core.storage.sql.Database;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.storage.sql.commandset.MySQLCommandSet;
import de.bluecolored.bluemap.core.storage.sql.commandset.PostgreSQLCommandSet;
import de.bluecolored.bluemap.core.storage.sql.commandset.SqliteCommandSet;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.function.Function;
public interface Dialect extends Keyed {
Dialect MYSQL = new Impl(Key.bluemap("mysql"), "jdbc:mysql:", MySQLCommandSet::new);
Dialect MARIADB = new Impl(Key.bluemap("mariadb"), "jdbc:mariadb:", MySQLCommandSet::new);
Dialect POSTGRESQL = new Impl(Key.bluemap("postgresql"), "jdbc:postgresql:", PostgreSQLCommandSet::new);
Dialect SQLITE = new Impl(Key.bluemap("sqlite"), "jdbc:sqlite:", SqliteCommandSet::new);
Registry<Dialect> REGISTRY = new Registry<>(
MYSQL,
MARIADB,
POSTGRESQL,
SQLITE
);
boolean supports(String connectionUrl);
CommandSet createCommandSet(Database database);
@RequiredArgsConstructor
class Impl implements Dialect {
@Getter private final Key key;
private final String protocol;
private final Function<Database, CommandSet> commandSetProvider;
@Override
public boolean supports(String connectionUrl) {
return connectionUrl.startsWith(protocol);
}
@Override
public CommandSet createCommandSet(Database database) {
return commandSetProvider.apply(database);
}
}
}

View File

@ -24,30 +24,30 @@
*/
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.file.FileStorageSettings;
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.file.FileStorage;
import lombok.Getter;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.nio.file.Path;
@SuppressWarnings("FieldMayBeFinal")
@DebugDump
@ConfigSerializable
public class FileConfig extends StorageConfig implements FileStorageSettings {
@Getter
public class FileConfig extends StorageConfig {
private Path root = Path.of("bluemap", "web", "maps");
private String compression = Compression.GZIP.getKey().getFormatted();
private boolean atomic = true;
private Compression compression = Compression.GZIP;
@Override
public Path getRoot() {
return root;
public Compression getCompression() throws ConfigurationException {
return parseKey(Compression.REGISTRY, compression, "compression");
}
@Override
public Compression getCompression() {
return compression;
public FileStorage createStorage() throws ConfigurationException {
return new FileStorage(root, getCompression(), atomic);
}
}

View File

@ -24,68 +24,153 @@
*/
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.sql.SQLStorageSettings;
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.common.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.sql.Database;
import de.bluecolored.bluemap.core.storage.sql.SQLStorage;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Driver;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@ConfigSerializable
public class SQLConfig extends StorageConfig implements SQLStorageSettings {
@Getter
public class SQLConfig extends StorageConfig {
@DebugDump private String driverJar = null;
@DebugDump private String driverClass = null;
@DebugDump(exclude = true)
private String connectionUrl = "jdbc:mysql://localhost/bluemap?permitMysqlScheme";
@DebugDump(exclude = true)
private Map<String, String> connectionProperties = new HashMap<>();
@DebugDump private Compression compression = Compression.GZIP;
private String dialect = null;
@DebugDump private transient URL driverJarURL = null;
private String driverJar = null;
private String driverClass = null;
private int maxConnections = -1;
@DebugDump private int maxConnections = -1;
private String compression = Compression.GZIP.getKey().getFormatted();
@Override
public Optional<URL> getDriverJar() throws MalformedURLException {
if (driverJar == null) return Optional.empty();
@Getter(AccessLevel.NONE)
private transient URL driverJarURL = null;
if (driverJarURL == null) {
driverJarURL = Paths.get(driverJar).toUri().toURL();
public Optional<URL> getDriverJar() throws ConfigurationException {
try {
if (driverJar == null) return Optional.empty();
if (driverJarURL == null) {
driverJarURL = Paths.get(driverJar).toUri().toURL();
}
return Optional.of(driverJarURL);
} catch (MalformedURLException ex) {
throw new ConfigurationException("""
The configured driver-jar path is not formatted correctly!
Please check your 'driver-jar' setting in your configuration and make sure you have the correct path configured.
""".strip(), ex);
}
return Optional.of(driverJarURL);
}
@Override
@SuppressWarnings("unused")
public Optional<String> getDriverClass() {
return Optional.ofNullable(driverClass);
}
@Override
public String getConnectionUrl() {
return connectionUrl;
public Compression getCompression() throws ConfigurationException {
return parseKey(Compression.REGISTRY, compression, "compression");
}
public Dialect getDialect() throws ConfigurationException {
String key = dialect;
// default from connection-url
if (key == null) {
for (Dialect d : Dialect.REGISTRY.values()) {
if (d.supports(connectionUrl)) {
key = d.getKey().getFormatted();
break;
}
}
if (key == null) throw new ConfigurationException("""
Could not find any sql-dialect that is matching the given connection-url.
Please check your 'connection-url' setting in your configuration and make sure it is in the correct format.
""".strip());
}
return parseKey(Dialect.REGISTRY, key, "dialect");
}
@Override
public Map<String, String> getConnectionProperties() {
return connectionProperties;
public SQLStorage createStorage() throws ConfigurationException {
Driver driver = createDriver();
Database database;
if (driver != null) {
database = new Database(getConnectionUrl(), getConnectionProperties(), getMaxConnections(), driver);
} else {
database = new Database(getConnectionUrl(), getConnectionProperties(), getMaxConnections());
}
CommandSet commandSet = getDialect().createCommandSet(database);
return new SQLStorage(commandSet, getCompression());
}
@Override
public int getMaxConnections() {
return maxConnections;
}
private @Nullable Driver createDriver() throws ConfigurationException {
if (driverClass == null) return null;
@Override
public Compression getCompression() {
return compression;
try {
// load driver class
Class<?> driverClazz;
URL driverJarUrl = getDriverJar().orElse(null);
if (driverJarUrl != null) {
// sanity-check if file exists
if (!Files.exists(Path.of(driverJarUrl.toURI()))) {
throw new ConfigurationException("""
The configured driver-jar was not found!
Please check your 'driver-jar' setting in your configuration and make sure you have the correct path configured.
""".strip());
}
ClassLoader classLoader = new URLClassLoader(new URL[]{driverJarUrl});
driverClazz = Class.forName(driverClass, true, classLoader);
} else {
driverClazz = Class.forName(driverClass);
}
// create driver
return (Driver) driverClazz.getDeclaredConstructor().newInstance();
} catch (ClassCastException ex) {
throw new ConfigurationException("""
The configured driver-class was found but is not of the correct class-type!
Please check your 'driver-class' setting in your configuration and make sure you have the correct class configured.
""".strip(), ex);
} catch (ClassNotFoundException ex) {
throw new ConfigurationException("""
The configured driver-class was not found!
Please check your 'driver-class' setting in your configuration and make sure you have the correct class configured.
""".strip(), ex);
} catch (ConfigurationException ex) {
throw ex;
} catch (Exception ex) {
throw new ConfigurationException("""
BlueMap failed to load the configured SQL-Driver!
Please check your 'driver-jar' and 'driver-class' settings in your configuration.
""".strip(), ex);
}
}
}

View File

@ -24,26 +24,50 @@
*/
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.util.Locale;
@SuppressWarnings("FieldMayBeFinal")
@DebugDump
@ConfigSerializable
public class StorageConfig {
public abstract class StorageConfig {
private StorageType storageType = StorageType.FILE;
private String storageType = StorageType.FILE.getKey().getFormatted();
public StorageType getStorageType() {
return storageType;
public StorageType getStorageType() throws ConfigurationException {
return parseKey(StorageType.REGISTRY, storageType, "storage-type");
}
public Storage createStorage() throws Exception {
if (this.getClass().equals(StorageConfig.class))
throw new UnsupportedOperationException("Can not create a Storage from the StorageConfig superclass.");
public abstract Storage createStorage() throws ConfigurationException;
static <T extends Keyed> T parseKey(Registry<T> registry, String key, String typeName) throws ConfigurationException {
T type = registry.get(Key.parse(key, Key.BLUEMAP_NAMESPACE));
if (type == null) {
// try legacy config format
Key legacyFormatKey = Key.bluemap(key.toLowerCase(Locale.ROOT));
type = registry.get(legacyFormatKey);
}
if (type == null)
throw new ConfigurationException("No " + typeName + " found for key: " + key + "!");
return type;
}
@ConfigSerializable
public static class Base extends StorageConfig {
@Override
public Storage createStorage() {
throw new UnsupportedOperationException();
}
return storageType.getStorageFactory(this.getClass()).provide(this);
}
}

View File

@ -24,41 +24,31 @@
*/
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.file.FileStorage;
import de.bluecolored.bluemap.core.storage.sql.SQLStorage;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
public enum StorageType {
public interface StorageType extends Keyed {
FILE (FileConfig.class, FileStorage::new),
SQL (SQLConfig.class, SQLStorage::new);
StorageType FILE = new Impl(Key.bluemap("file"), FileConfig.class);
StorageType SQL = new Impl(Key.bluemap("sql"), SQLConfig.class);
private final Class<? extends StorageConfig> configType;
private final StorageFactory<? extends StorageConfig> storageFactory;
Registry<StorageType> REGISTRY = new Registry<>(
FILE,
SQL
);
<C extends StorageConfig> StorageType(Class<C> configType, StorageFactory<C> storageFactory) {
this.configType = configType;
this.storageFactory = storageFactory;
}
Class<? extends StorageConfig> getConfigType();
public Class<? extends StorageConfig> getConfigType() {
return configType;
}
@RequiredArgsConstructor
@Getter
class Impl implements StorageType {
@SuppressWarnings("unchecked")
public <C extends StorageConfig> StorageFactory<C> getStorageFactory(Class<C> configType) {
if (!configType.isAssignableFrom(this.configType)) throw new ClassCastException(this.configType + " can not be cast to " + configType);
return (StorageFactory<C>) storageFactory;
}
private final Key key;
private final Class<? extends StorageConfig> configType;
@FunctionalInterface
public interface StorageFactory<C extends StorageConfig> {
Storage provideRaw(C config) throws Exception;
@SuppressWarnings("unchecked")
default Storage provide(StorageConfig config) throws Exception {
return provideRaw((C) config);
}
}
}

View File

@ -22,39 +22,27 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.mca;
package de.bluecolored.bluemap.common.config.typeserializer;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.util.Key;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer;
public class EmptyChunk extends MCAChunk {
import java.lang.reflect.Type;
public static final MCAChunk INSTANCE = new EmptyChunk();
public class KeyTypeSerializer implements TypeSerializer<Key> {
@Override
public boolean isGenerated() {
return false;
public Key deserialize(Type type, ConfigurationNode node) {
String formatted = node.getString();
return formatted != null ? new Key(node.getString()) : null;
}
@Override
public long getInhabitedTime() {
return 0;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
return BlockState.AIR;
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
return target.set(0, 0);
}
@Override
public String getBiome(int x, int y, int z) {
return Biome.DEFAULT.getFormatted();
public void serialize(Type type, @Nullable Key obj, ConfigurationNode node) throws SerializationException {
if (obj != null) node.set(obj.getFormatted());
}
}

View File

@ -0,0 +1,44 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.debug;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.TYPE
})
public @interface DebugDump {
String value() default "";
boolean exclude() default false;
}

View File

@ -0,0 +1,332 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.debug;
import com.google.gson.stream.JsonWriter;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.util.Key;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.util.*;
public class StateDumper {
private static final StateDumper GLOBAL = new StateDumper();
private final Set<Object> instances = Collections.newSetFromMap(new WeakHashMap<>());
public void dump(Path file) throws IOException {
JsonWriter writer = new JsonWriter(Files.newBufferedWriter(
file,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
));
writer.setIndent(" ");
writer.beginObject();
writer.name("system-info");
collectSystemInfo(writer);
Set<Object> alreadyDumped = Collections.newSetFromMap(new IdentityHashMap<>());
writer.name("threads").beginArray();
for (Thread thread : Thread.getAllStackTraces().keySet()) {
dumpInstance(thread, writer, alreadyDumped);
}
writer.endArray();
writer.name("dump").beginObject();
for (Object instance : instances) {
Class<?> type = instance.getClass();
writer.name(type.getName());
dumpInstance(instance, writer, alreadyDumped);
}
writer.endObject();
writer.endObject();
writer.flush();
writer.close();
}
private void dumpInstance(Object instance, JsonWriter writer, Set<Object> alreadyDumped) throws IOException {
if (instance == null) {
writer.nullValue();
return;
}
if (instance instanceof String ||
instance instanceof Path ||
instance instanceof UUID ||
instance instanceof Key
) {
writer.value(instance.toString());
return;
}
if (instance instanceof Number val) {
writer.value(val);
return;
}
if (instance instanceof Boolean val) {
writer.value(val);
return;
}
if (!alreadyDumped.add(instance)) {
writer.value("<<" + toIdentityString(instance) + ">>");
return;
}
writer.beginObject();
try {
String identityString = toIdentityString(instance);
writer.name("#identity").value(identityString);
if (instance instanceof Map<?, ?> map) {
writer.name("entries").beginArray();
int count = 0;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (++count > 30) {
writer.value("<<" + (map.size() - 30) + " more elements>>");
break;
}
writer.beginObject();
writer.name("key");
dumpInstance(entry.getKey(), writer, alreadyDumped);
writer.name("value");
dumpInstance(entry.getValue(), writer, alreadyDumped);
writer.endObject();
}
writer.endArray();
return;
}
if (instance instanceof Collection<?> collection) {
writer.name("entries").beginArray();
int count = 0;
for (Object entry : collection) {
if (++count > 30) {
writer.value("<<" + (collection.size() - 30) + " more elements>>");
break;
}
dumpInstance(entry, writer, alreadyDumped);
}
writer.endArray();
return;
}
if (instance instanceof Object[] array) {
writer.name("entries").beginArray();
int count = 0;
for (Object entry : array) {
if (++count > 30) {
writer.value("<<" + (array.length - 30) + " more elements>>");
break;
}
dumpInstance(entry, writer, alreadyDumped);
}
writer.endArray();
return;
}
String toString = instance.toString();
if (!toString.equals(identityString))
writer.name("#toString").value(instance.toString());
if (instance instanceof Thread thread) {
writer.name("name").value(thread.getName());
writer.name("state").value(thread.getState().toString());
writer.name("priority").value(thread.getPriority());
writer.name("alive").value(thread.isAlive());
writer.name("id").value(thread.getId());
writer.name("deamon").value(thread.isDaemon());
writer.name("interrupted").value(thread.isInterrupted());
try {
StackTraceElement[] trace = thread.getStackTrace();
writer.name("stacktrace").beginArray();
for (StackTraceElement element : trace) {
writer.value(element.toString());
}
writer.endArray();
} catch (SecurityException ignore) {}
return;
}
dumpAnnotatedInstance(instance.getClass(), instance, writer, alreadyDumped);
} finally {
writer.endObject();
}
}
private static String toIdentityString(Object instance) {
return instance.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(instance));
}
private void dumpAnnotatedInstance(Class<?> type, Object instance, JsonWriter writer, Set<Object> alreadyDumped) throws IOException {
DebugDump typedd = type.getAnnotation(DebugDump.class);
boolean exclude = typedd != null && typedd.exclude();
boolean allFields = !exclude && (
typedd != null ||
type.getPackageName().startsWith("de.bluecolored.bluemap")
);
for (Field field : type.getDeclaredFields()) {
String key = field.getName();
Object value;
try {
DebugDump dd = field.getAnnotation(DebugDump.class);
if (dd == null) {
if (!allFields) continue;
if (Modifier.isStatic(field.getModifiers())) continue;
if (Modifier.isTransient(field.getModifiers())) continue;
} else {
if (dd.exclude()) continue;
}
if (dd != null) {
key = dd.value();
if (key.isEmpty()) key = field.getName();
}
field.setAccessible(true);
value = field.get(instance);
} catch (Exception ex) {
writer.name("!!" + key).value(ex.toString());
continue;
}
writer.name(key);
dumpInstance(value, writer, alreadyDumped);
}
for (Method method : type.getDeclaredMethods()) {
String key = method.toGenericString();
Object value;
try {
DebugDump dd = method.getAnnotation(DebugDump.class);
if (dd == null || dd.exclude()) continue;
key = dd.value();
if (key.isEmpty()) key = method.toGenericString();
method.setAccessible(true);
value = method.invoke(instance);
} catch (Exception ex) {
writer.name("!!" + key).value(ex.toString());
continue;
}
writer.name(key);
dumpInstance(value, writer, alreadyDumped);
}
for (Class<?> iface : type.getInterfaces()) {
dumpAnnotatedInstance(iface, instance, writer, alreadyDumped);
}
Class<?> typeSuperclass = type.getSuperclass();
if (typeSuperclass != null) {
dumpAnnotatedInstance(typeSuperclass, instance, writer, alreadyDumped);
}
}
private void collectSystemInfo(JsonWriter writer) throws IOException {
writer.beginObject();
writer.name("bluemap-version").value(BlueMap.VERSION);
writer.name("git-hash").value(BlueMap.GIT_HASH);
String[] properties = new String[]{
"java.runtime.name",
"java.runtime.version",
"java.vm.vendor",
"java.vm.name",
"os.name",
"os.version",
"user.dir",
"java.home",
"file.separator",
"sun.io.unicode.encoding",
"java.class.version"
};
Map<String, String> propMap = new HashMap<>();
for (String key : properties) {
propMap.put(key, System.getProperty(key));
}
writer.name("properties");
dumpInstance(propMap, writer, new HashSet<>());
writer.name("cores").value(Runtime.getRuntime().availableProcessors());
writer.name("max-memory").value(Runtime.getRuntime().maxMemory());
writer.name("total-memory").value(Runtime.getRuntime().totalMemory());
writer.name("free-memory").value(Runtime.getRuntime().freeMemory());
writer.name("timestamp").value(System.currentTimeMillis());
writer.name("time").value(LocalDateTime.now().toString());
writer.endObject();
}
public static StateDumper global() {
return GLOBAL;
}
public synchronized void register(Object instance) {
GLOBAL.instances.add(instance);
}
}

View File

@ -27,9 +27,9 @@ package de.bluecolored.bluemap.common.live;
import com.google.gson.stream.JsonWriter;
import de.bluecolored.bluemap.common.config.PluginConfig;
import de.bluecolored.bluemap.common.serverinterface.Player;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.logger.Logger;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.StringWriter;
@ -39,15 +39,15 @@ import java.util.function.Supplier;
public class LivePlayersDataSupplier implements Supplier<String> {
private final ServerInterface server;
private final Server server;
private final PluginConfig config;
@Nullable private final String worldId;
private final ServerWorld world;
private final Predicate<UUID> playerFilter;
public LivePlayersDataSupplier(ServerInterface server, PluginConfig config, @Nullable String worldId, Predicate<UUID> playerFilter) {
public LivePlayersDataSupplier(Server server, PluginConfig config, ServerWorld world, Predicate<UUID> playerFilter) {
this.server = server;
this.config = config;
this.worldId = worldId;
this.world = world;
this.playerFilter = playerFilter;
}
@ -61,9 +61,7 @@ public class LivePlayersDataSupplier implements Supplier<String> {
if (config.isLivePlayerMarkers()) {
for (Player player : this.server.getOnlinePlayers()) {
if (!player.isOnline()) continue;
boolean isCorrectWorld = player.getWorld().equals(this.worldId);
boolean isCorrectWorld = player.getWorld().equals(this.world);
if (config.isHideInvisible() && player.isInvisible()) continue;
if (config.isHideVanished() && player.isVanished()) continue;

View File

@ -0,0 +1,115 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.plugin;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.util.WatchService;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
public class MapUpdateService extends Thread {
private final BmMap map;
private final RenderManager renderManager;
private final WatchService<Vector2i> watchService;
private volatile boolean closed;
private Timer delayTimer;
private final Map<Vector2i, TimerTask> scheduledUpdates;
public MapUpdateService(RenderManager renderManager, BmMap map) throws IOException {
this.renderManager = renderManager;
this.map = map;
this.closed = false;
this.scheduledUpdates = new HashMap<>();
this.watchService = map.getWorld().createRegionWatchService();
}
@Override
public void run() {
if (delayTimer == null) delayTimer = new Timer("BlueMap-RegionFileWatchService-DelayTimer", true);
Logger.global.logDebug("Started watching map '" + map.getId() + "' for updates...");
try {
while (!closed)
this.watchService.take().forEach(this::updateRegion);
} catch (WatchService.ClosedException ignore) {
} catch (InterruptedException iex) {
Thread.currentThread().interrupt();
} finally {
Logger.global.logDebug("Stopped watching map '" + map.getId() + "' for updates.");
if (!closed) {
Logger.global.logWarning("Region-file watch-service for map '" + map.getId() +
"' stopped unexpectedly! (This map might not update automatically from now on)");
}
}
}
private synchronized void updateRegion(Vector2i regionPos) {
// we only want to start the render when there were no changes on a file for 5 seconds
TimerTask task = scheduledUpdates.remove(regionPos);
if (task != null) task.cancel();
task = new TimerTask() {
@Override
public void run() {
synchronized (MapUpdateService.this) {
WorldRegionRenderTask task = new WorldRegionRenderTask(map, regionPos);
scheduledUpdates.remove(regionPos);
renderManager.scheduleRenderTask(task);
Logger.global.logDebug("Scheduled update for region-file: " + regionPos + " (Map: " + map.getId() + ")");
}
}
};
scheduledUpdates.put(regionPos, task);
delayTimer.schedule(task, 5000);
}
public void close() {
this.closed = true;
this.interrupt();
if (this.delayTimer != null) this.delayTimer.cancel();
try {
this.watchService.close();
} catch (Exception ex) {
Logger.global.logError("Exception while trying to close WatchService!", ex);
}
}
}

View File

@ -24,29 +24,34 @@
*/
package de.bluecolored.bluemap.common.plugin;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.BlueMapConfigProvider;
import de.bluecolored.bluemap.common.BlueMapConfiguration;
import de.bluecolored.bluemap.common.BlueMapService;
import de.bluecolored.bluemap.common.InterruptableReentrantLock;
import de.bluecolored.bluemap.common.MissingResourcesException;
import de.bluecolored.bluemap.common.addons.Addons;
import de.bluecolored.bluemap.common.api.BlueMapAPIImpl;
import de.bluecolored.bluemap.common.config.*;
import de.bluecolored.bluemap.common.debug.StateDumper;
import de.bluecolored.bluemap.common.live.LivePlayersDataSupplier;
import de.bluecolored.bluemap.common.plugin.skins.PlayerSkinUpdater;
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.common.web.*;
import de.bluecolored.bluemap.common.web.http.HttpServer;
import de.bluecolored.bluemap.core.debug.StateDumper;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.metrics.Metrics;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.MinecraftVersion;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.world.World;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.serialize.SerializationException;
@ -55,46 +60,49 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@DebugDump
@Getter
public class Plugin implements ServerEventListener {
public static final String PLUGIN_ID = "bluemap";
public static final String PLUGIN_NAME = "BlueMap";
private static final String DEBUG_FILE_LOG_NAME = "file-debug-log";
@Getter(AccessLevel.NONE)
private final InterruptableReentrantLock loadingLock = new InterruptableReentrantLock();
private final String implementationType;
private final ServerInterface serverInterface;
private final Server serverInterface;
private BlueMapService blueMap;
private PluginState pluginState;
private Map<String, World> worlds;
private Map<String, BmMap> maps;
private RenderManager renderManager;
private HttpServer webServer;
private BlueMapAPIImpl api;
private HttpServer webServer;
private RoutingRequestHandler webRequestHandler;
private Logger webLogger;
private Timer daemonTimer;
private Map<String, RegionFileWatchService> regionFileWatchServices;
private Map<String, MapUpdateService> mapUpdateServices;
private PlayerSkinUpdater skinUpdater;
private boolean loaded = false;
public Plugin(String implementationType, ServerInterface serverInterface) {
public Plugin(String implementationType, Server serverInterface) {
this.implementationType = implementationType.toLowerCase();
this.serverInterface = serverInterface;
@ -113,12 +121,36 @@ public class Plugin implements ServerEventListener {
if (loaded) return;
unload(); //ensure nothing is left running (from a failed load or something)
//load addons
Path addonsFolder = serverInterface.getConfigFolder().resolve("addons");
Files.createDirectories(addonsFolder);
Addons.tryLoadAddons(addonsFolder, true);
//serverInterface.getModsFolder().ifPresent(Addons::tryLoadAddons);
//load configs
blueMap = new BlueMapService(serverInterface, new BlueMapConfigs(serverInterface), preloadedResourcePack);
CoreConfig coreConfig = getConfigs().getCoreConfig();
WebserverConfig webserverConfig = getConfigs().getWebserverConfig();
WebappConfig webappConfig = getConfigs().getWebappConfig();
PluginConfig pluginConfig = getConfigs().getPluginConfig();
BlueMapConfigManager configManager = BlueMapConfigManager.builder()
.minecraftVersion(serverInterface.getMinecraftVersion())
.configRoot(serverInterface.getConfigFolder())
.packsFolder(serverInterface.getConfigFolder().resolve("packs"))
.modsFolder(serverInterface.getModsFolder().orElse(null))
.useMetricsConfig(serverInterface.isMetricsEnabled() == Tristate.UNDEFINED)
.autoConfigWorlds(serverInterface.getLoadedServerWorlds())
.build();
CoreConfig coreConfig = configManager.getCoreConfig();
WebserverConfig webserverConfig = configManager.getWebserverConfig();
WebappConfig webappConfig = configManager.getWebappConfig();
PluginConfig pluginConfig = configManager.getPluginConfig();
//apply new file-logger config
if (coreConfig.getLog().getFile() != null) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
Logger.global.put(DEBUG_FILE_LOG_NAME, () -> Logger.file(
Path.of(String.format(coreConfig.getLog().getFile(), zdt)),
coreConfig.getLog().isAppend()
));
} else {
Logger.global.remove(DEBUG_FILE_LOG_NAME);
}
//load plugin state
try {
@ -131,16 +163,19 @@ public class Plugin implements ServerEventListener {
pluginState = new PluginState();
}
//create bluemap-service
blueMap = new BlueMapService(configManager, preloadedResourcePack);
//try load resources
try {
blueMap.getResourcePack();
blueMap.getOrLoadResourcePack();
} catch (MissingResourcesException ex) {
Logger.global.logWarning("BlueMap is missing important resources!");
Logger.global.logWarning("You must accept the required file download in order for BlueMap to work!");
BlueMapConfigProvider configProvider = blueMap.getConfigs();
if (configProvider instanceof BlueMapConfigs) {
Logger.global.logWarning("Please check: " + ((BlueMapConfigs) configProvider).getConfigManager().findConfigPath(Path.of("core")).toAbsolutePath().normalize());
BlueMapConfiguration configProvider = blueMap.getConfig();
if (configProvider instanceof BlueMapConfigManager) {
Logger.global.logWarning("Please check: " + ((BlueMapConfigManager) configProvider).getConfigManager().resolveConfigFile(BlueMapConfigManager.CORE_CONFIG_NAME).toAbsolutePath().normalize());
}
Logger.global.logInfo("If you have changed the config you can simply reload the plugin using: /bluemap reload");
@ -149,22 +184,21 @@ public class Plugin implements ServerEventListener {
return;
}
//load worlds and maps
worlds = blueMap.getWorlds();
maps = blueMap.getMaps();
//load maps
Map<String, BmMap> maps = blueMap.getOrLoadMaps();
//create and start webserver
if (webserverConfig.isEnabled()) {
Path webroot = webserverConfig.getWebroot();
FileHelper.createDirectories(webroot);
RoutingRequestHandler routingRequestHandler = new RoutingRequestHandler();
this.webRequestHandler = new RoutingRequestHandler();
// default route
routingRequestHandler.register(".*", new FileRequestHandler(webroot));
webRequestHandler.register(".*", new FileRequestHandler(webroot));
// map route
for (var mapConfigEntry : getConfigs().getMapConfigs().entrySet()) {
for (var mapConfigEntry : configManager.getMapConfigs().entrySet()) {
String id = mapConfigEntry.getKey();
MapConfig mapConfig = mapConfigEntry.getValue();
@ -173,19 +207,34 @@ public class Plugin implements ServerEventListener {
if (map != null) {
mapRequestHandler = new MapRequestHandler(map, serverInterface, pluginConfig, Predicate.not(pluginState::isPlayerHidden));
} else {
Storage storage = blueMap.getStorage(mapConfig.getStorage());
mapRequestHandler = new MapRequestHandler(id, storage);
Storage storage = blueMap.getOrLoadStorage(mapConfig.getStorage());
mapRequestHandler = new MapRequestHandler(storage.map(id));
}
routingRequestHandler.register(
webRequestHandler.register(
"maps/" + Pattern.quote(id) + "/(.*)",
"$1",
new BlueMapResponseModifier(mapRequestHandler)
);
}
// create web-logger
List<Logger> webLoggerList = new ArrayList<>();
if (webserverConfig.getLog().getFile() != null) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
webLoggerList.add(Logger.file(
Path.of(String.format(webserverConfig.getLog().getFile(), zdt)),
webserverConfig.getLog().isAppend()
));
}
webLogger = Logger.combine(webLoggerList);
try {
webServer = new HttpServer(routingRequestHandler);
webServer = new HttpServer(new LoggingRequestHandler(
webRequestHandler,
webserverConfig.getLog().getFormat(),
webLogger
));
webServer.bind(new InetSocketAddress(
webserverConfig.resolveIp(),
webserverConfig.getPort()
@ -194,10 +243,15 @@ public class Plugin implements ServerEventListener {
} catch (UnknownHostException ex) {
throw new ConfigurationException("BlueMap failed to resolve the ip in your webserver-config.\n" +
"Check if that is correctly configured.", ex);
} catch (BindException ex) {
throw new ConfigurationException("BlueMap failed to bind to the configured address.\n" +
"This usually happens when the configured port (" + webserverConfig.getPort() + ") is already in use by some other program.", ex);
} catch (IOException ex) {
throw new ConfigurationException("BlueMap failed to initialize the webserver.\n" +
"Check your webserver-config if everything is configured correctly.\n" +
"(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)", ex);
throw new ConfigurationException("""
BlueMap failed to initialize the webserver.
Check your webserver-config if everything is configured correctly.
(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)
""".strip(), ex);
}
}
@ -220,20 +274,13 @@ public class Plugin implements ServerEventListener {
}
});
//start render-manager
if (pluginState.isRenderThreadsEnabled()) {
checkPausedByPlayerCount(); // <- this also starts the render-manager if it should start
} else {
Logger.global.logInfo("Render-Threads are STOPPED! Use the command 'bluemap start' to start them.");
}
//update webapp and settings
if (webappConfig.isEnabled())
blueMap.createOrUpdateWebApp(false);
//start skin updater
this.skinUpdater = new PlayerSkinUpdater(this);
if (pluginConfig.isLivePlayerMarkers()) {
this.skinUpdater = new PlayerSkinUpdater(this);
serverInterface.registerListener(skinUpdater);
}
@ -247,7 +294,7 @@ public class Plugin implements ServerEventListener {
save();
}
};
daemonTimer.schedule(saveTask, TimeUnit.MINUTES.toMillis(2), TimeUnit.MINUTES.toMillis(2));
daemonTimer.schedule(saveTask, TimeUnit.MINUTES.toMillis(10), TimeUnit.MINUTES.toMillis(10));
//periodically save markers
int writeMarkersInterval = pluginConfig.getWriteMarkersInterval();
@ -277,8 +324,8 @@ public class Plugin implements ServerEventListener {
TimerTask fileWatcherRestartTask = new TimerTask() {
@Override
public void run() {
regionFileWatchServices.values().forEach(RegionFileWatchService::close);
regionFileWatchServices.clear();
mapUpdateServices.values().forEach(MapUpdateService::close);
mapUpdateServices.clear();
initFileWatcherTasks();
}
};
@ -301,17 +348,18 @@ public class Plugin implements ServerEventListener {
}
//metrics
MinecraftVersion minecraftVersion = blueMap.getOrLoadMinecraftVersion();
TimerTask metricsTask = new TimerTask() {
@Override
public void run() {
if (Plugin.this.serverInterface.isMetricsEnabled().getOr(coreConfig::isMetrics))
Metrics.sendReport(Plugin.this.implementationType);
if (serverInterface.isMetricsEnabled().getOr(coreConfig::isMetrics))
Metrics.sendReport(implementationType, minecraftVersion.getId());
}
};
daemonTimer.scheduleAtFixedRate(metricsTask, TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(30));
//watch map-changes
this.regionFileWatchServices = new HashMap<>();
this.mapUpdateServices = new HashMap<>();
initFileWatcherTasks();
//register listener
@ -321,9 +369,12 @@ public class Plugin implements ServerEventListener {
this.api = new BlueMapAPIImpl(this);
this.api.register();
//save webapp settings again (for api-registered scripts and styles)
if (webappConfig.isEnabled())
this.getBlueMap().getWebFilesManager().saveSettings();
//start render-manager
if (pluginState.isRenderThreadsEnabled()) {
checkPausedByPlayerCount(); // <- this also starts the render-manager if it should start
} else {
Logger.global.logInfo("Render-Threads are STOPPED! Use the command 'bluemap start' to start them.");
}
//done
loaded = true;
@ -342,12 +393,11 @@ public class Plugin implements ServerEventListener {
public void unload() {
this.unload(false);
}
public void unload(boolean keepWebserver) {
loadingLock.interruptAndLock();
try {
synchronized (this) {
//save
save();
//disable api
if (api != null) api.unregister();
@ -362,14 +412,24 @@ public class Plugin implements ServerEventListener {
daemonTimer = null;
//stop file-watchers
if (regionFileWatchServices != null) {
regionFileWatchServices.values().forEach(RegionFileWatchService::close);
regionFileWatchServices.clear();
if (mapUpdateServices != null) {
mapUpdateServices.values().forEach(MapUpdateService::close);
mapUpdateServices.clear();
}
regionFileWatchServices = null;
mapUpdateServices = null;
//stop services
// stop render-manager
if (renderManager != null){
if (renderManager.getCurrentRenderTask() != null) {
renderManager.removeAllRenderTasks();
if (!renderManager.isRunning()) renderManager.start(1);
try {
renderManager.awaitIdle(true);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
renderManager.stop();
try {
renderManager.awaitShutdown();
@ -377,8 +437,11 @@ public class Plugin implements ServerEventListener {
Thread.currentThread().interrupt();
}
}
renderManager = null;
//save
save();
// stop webserver
if (webServer != null && !keepWebserver) {
try {
webServer.close();
@ -388,6 +451,15 @@ public class Plugin implements ServerEventListener {
webServer = null;
}
if (webLogger != null && !keepWebserver) {
try {
webLogger.close();
} catch (Exception ex) {
Logger.global.logError("Failed to close the webserver-logger!", ex);
}
webLogger = null;
}
//close bluemap
if (blueMap != null) {
try {
@ -398,10 +470,10 @@ public class Plugin implements ServerEventListener {
}
blueMap = null;
//clear resources
worlds = null;
maps = null;
// remove file-logger
Logger.global.remove(DEBUG_FILE_LOG_NAME);
//clear resources
pluginState = null;
//done
@ -431,7 +503,7 @@ public class Plugin implements ServerEventListener {
}
// hold and reuse loaded resourcepack
ResourcePack preloadedResourcePack = this.blueMap.getResourcePackIfLoaded().orElse(null);
ResourcePack preloadedResourcePack = this.blueMap.getResourcePack();
unload();
load(preloadedResourcePack);
@ -443,10 +515,12 @@ public class Plugin implements ServerEventListener {
}
public synchronized void save() {
if (blueMap == null) return;
if (pluginState != null) {
try {
GsonConfigurationLoader loader = GsonConfigurationLoader.builder()
.path(blueMap.getConfigs().getCoreConfig().getData().resolve("pluginState.json"))
.path(blueMap.getConfig().getCoreConfig().getData().resolve("pluginState.json"))
.build();
loader.save(loader.createNode().set(PluginState.class, pluginState));
} catch (IOException ex) {
@ -454,38 +528,41 @@ public class Plugin implements ServerEventListener {
}
}
if (maps != null) {
for (BmMap map : maps.values()) {
map.save();
}
var maps = blueMap.getMaps();
for (BmMap map : maps.values()) {
map.save();
}
}
public void saveMarkerStates() {
if (maps != null) {
for (BmMap map : maps.values()) {
map.saveMarkerState();
}
if (blueMap == null) return;
var maps = blueMap.getMaps();
for (BmMap map : maps.values()) {
map.saveMarkerState();
}
}
public void savePlayerStates() {
if (maps != null) {
for (BmMap map : maps.values()) {
var dataSupplier = new LivePlayersDataSupplier(
serverInterface,
getConfigs().getPluginConfig(),
map.getWorldId(),
Predicate.not(pluginState::isPlayerHidden)
);
try (
OutputStream out = map.getStorage().writeMeta(map.getId(), BmMap.META_FILE_PLAYERS);
Writer writer = new OutputStreamWriter(out)
) {
writer.write(dataSupplier.get());
} catch (Exception ex) {
Logger.global.logError("Failed to save players for map '" + map.getId() + "'!", ex);
}
if (blueMap == null) return;
var maps = blueMap.getMaps();
for (BmMap map : maps.values()) {
var serverWorld = serverInterface.getServerWorld(map.getWorld()).orElse(null);
if (serverWorld == null) continue;
var dataSupplier = new LivePlayersDataSupplier(
serverInterface,
getBlueMap().getConfig().getPluginConfig(),
serverWorld,
Predicate.not(pluginState::isPlayerHidden)
);
try (
OutputStream out = map.getStorage().players().write();
Writer writer = new OutputStreamWriter(out)
) {
writer.write(dataSupplier.get());
} catch (Exception ex) {
Logger.global.logError("Failed to save players for map '" + map.getId() + "'!", ex);
}
}
}
@ -494,23 +571,27 @@ public class Plugin implements ServerEventListener {
stopWatchingMap(map);
try {
RegionFileWatchService watcher = new RegionFileWatchService(renderManager, map, false);
MapUpdateService watcher = new MapUpdateService(renderManager, map);
watcher.start();
regionFileWatchServices.put(map.getId(), watcher);
mapUpdateServices.put(map.getId(), watcher);
} catch (IOException ex) {
Logger.global.logError("Failed to create file-watcher for map: " + map.getId() + " (This means the map might not automatically update)", ex);
Logger.global.logError("Failed to create update-watcher for map: " + map.getId() +
" (This means the map might not automatically update)", ex);
} catch (UnsupportedOperationException ex) {
Logger.global.logWarning("Update-watcher for map '" + map.getId() + "' is not supported for the world-type." +
" (This means the map might not automatically update)");
}
}
public synchronized void stopWatchingMap(BmMap map) {
RegionFileWatchService watcher = regionFileWatchServices.remove(map.getId());
MapUpdateService watcher = mapUpdateServices.remove(map.getId());
if (watcher != null) {
watcher.close();
}
}
public boolean flushWorldUpdates(World world) throws IOException {
var implWorld = serverInterface.getWorld(world.getSaveFolder()).orElse(null);
var implWorld = serverInterface.getServerWorld(world).orElse(null);
if (implWorld != null) return implWorld.persistWorldChanges();
return false;
}
@ -540,8 +621,8 @@ public class Plugin implements ServerEventListener {
}
public boolean checkPausedByPlayerCount() {
CoreConfig coreConfig = getConfigs().getCoreConfig();
PluginConfig pluginConfig = getConfigs().getPluginConfig();
CoreConfig coreConfig = getBlueMap().getConfig().getCoreConfig();
PluginConfig pluginConfig = getBlueMap().getConfig().getPluginConfig();
if (
pluginConfig.getPlayerRenderLimit() > 0 &&
@ -556,54 +637,18 @@ public class Plugin implements ServerEventListener {
}
}
public ServerInterface getServerInterface() {
return serverInterface;
}
public BlueMapService getBlueMap() {
return blueMap;
}
public BlueMapConfigProvider getConfigs() {
return blueMap.getConfigs();
}
public PluginState getPluginState() {
return pluginState;
}
public Map<String, World> getWorlds(){
return worlds;
}
public Map<String, BmMap> getMaps(){
return maps;
}
public RenderManager getRenderManager() {
return renderManager;
}
public HttpServer getWebServer() {
return webServer;
}
public boolean isLoaded() {
return loaded;
}
public String getImplementationType() {
return implementationType;
}
public PlayerSkinUpdater getSkinUpdater() {
return skinUpdater;
public @Nullable World getWorld(ServerWorld serverWorld) {
String id = World.id(serverWorld.getWorldFolder(), serverWorld.getDimension());
return getBlueMap().getWorlds().get(id);
}
private void initFileWatcherTasks() {
for (BmMap map : maps.values()) {
if (pluginState.getMapState(map).isUpdateEnabled()) {
startWatchingMap(map);
var maps = blueMap.getMaps();
if (maps != null) {
for (BmMap map : maps.values()) {
if (pluginState.getMapState(map).isUpdateEnabled()) {
startWatchingMap(map);
}
}
}
}

View File

@ -1,151 +0,0 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.plugin;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.util.FileHelper;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
public class RegionFileWatchService extends Thread {
private final BmMap map;
private final RenderManager renderManager;
private final WatchService watchService;
private boolean verbose;
private volatile boolean closed;
private Timer delayTimer;
@DebugDump
private final Map<Vector2i, TimerTask> scheduledUpdates;
public RegionFileWatchService(RenderManager renderManager, BmMap map, boolean verbose) throws IOException {
this.renderManager = renderManager;
this.map = map;
this.verbose = verbose;
this.closed = false;
this.scheduledUpdates = new HashMap<>();
Path folder = map.getWorld().getSaveFolder().resolve("region");
FileHelper.createDirectories(folder);
this.watchService = folder.getFileSystem().newWatchService();
folder.register(this.watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY);
}
@Override
public void run() {
if (delayTimer == null) delayTimer = new Timer("BlueMap-RegionFileWatchService-DelayTimer", true);
try {
while (!closed) {
WatchKey key = this.watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) continue;
Object fileObject = event.context();
if (!(fileObject instanceof Path)) continue;
Path file = (Path) fileObject;
String regionFileName = file.toFile().getName();
updateRegion(regionFileName);
}
if (!key.reset()) return;
}
} catch ( ClosedWatchServiceException ignore) {
} catch (InterruptedException iex) {
Thread.currentThread().interrupt();
}
if (!closed) {
Logger.global.logWarning("Region-file watch-service for map '" + map.getId() +
"' stopped unexpectedly! (This map might not update automatically from now on)");
}
}
private synchronized void updateRegion(String regionFileName) {
if (!regionFileName.endsWith(".mca")) return;
if (!regionFileName.startsWith("r.")) return;
try {
String[] filenameParts = regionFileName.split("\\.");
if (filenameParts.length < 3) return;
int rX = Integer.parseInt(filenameParts[1]);
int rZ = Integer.parseInt(filenameParts[2]);
Vector2i regionPos = new Vector2i(rX, rZ);
// we only want to start the render when there were no changes on a file for 10 seconds
TimerTask task = scheduledUpdates.remove(regionPos);
if (task != null) task.cancel();
task = new TimerTask() {
@Override
public void run() {
synchronized (RegionFileWatchService.this) {
WorldRegionRenderTask task = new WorldRegionRenderTask(map, regionPos);
scheduledUpdates.remove(regionPos);
renderManager.scheduleRenderTask(task);
if (verbose) Logger.global.logInfo("Scheduled update for region-file: " + regionPos + " (Map: " + map.getId() + ")");
}
}
};
scheduledUpdates.put(regionPos, task);
delayTimer.schedule(task, 10000);
} catch (NumberFormatException ignore) {}
}
public void close() {
this.closed = true;
this.interrupt();
if (this.delayTimer != null) this.delayTimer.cancel();
try {
this.watchService.close();
} catch (IOException ex) {
Logger.global.logError("Exception while trying to close WatchService!", ex);
}
}
}

View File

@ -30,14 +30,22 @@ import de.bluecolored.bluemap.common.plugin.text.TextColor;
import de.bluecolored.bluemap.common.plugin.text.TextFormat;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.RenderTask;
import de.bluecolored.bluemap.core.world.World;
import org.apache.commons.lang3.time.DurationFormatUtils;
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
public class CommandHelper {
private static final DateTimeFormatter TIME_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withLocale(Locale.ROOT)
.withZone(ZoneId.systemDefault());
private final Plugin plugin;
private final Map<String, WeakReference<RenderTask>> taskRefMap;
@ -67,7 +75,9 @@ public class CommandHelper {
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ", status, TextColor.WHITE, "!"));
if (!tasks.isEmpty()) {
if (tasks.isEmpty()) {
lines.add(Text.of(TextColor.GRAY, " Last time running: ", TextColor.DARK_GRAY, formatTime(renderer.getLastTimeBusy())));
} else {
lines.add(Text.of(TextColor.WHITE, " Queued Tasks (" + tasks.size() + "):"));
for (int i = 0; i < tasks.size(); i++) {
if (i >= 10){
@ -76,20 +86,24 @@ public class CommandHelper {
}
RenderTask task = tasks.get(i);
lines.add(Text.of(TextColor.GRAY, " [" + getRefForTask(task) + "] ", TextColor.GOLD, task.getDescription()));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0[" + getRefForTask(task) + "] ", TextColor.GOLD, task.getDescription()));
if (i == 0) {
String detail = task.getDetail().orElse(null);
if (detail != null) {
lines.add(Text.of(TextColor.GRAY, " Detail: ", TextColor.WHITE, detail));
}
task.getDetail().ifPresent(detail ->
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0Detail: ", TextColor.WHITE, detail)));
lines.add(Text.of(TextColor.GRAY, " Progress: ", TextColor.WHITE,
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0Progress: ", TextColor.WHITE,
(Math.round(task.estimateProgress() * 10000) / 100.0) + "%"));
long etaMs = renderer.estimateCurrentRenderTaskTimeRemaining();
if (etaMs > 0) {
lines.add(Text.of(TextColor.GRAY, " ETA: ", TextColor.WHITE, DurationFormatUtils.formatDuration(etaMs, "HH:mm:ss")));
Duration eta = Duration.of(etaMs, ChronoUnit.MILLIS);
String etaString = "%d:%02d:%02d".formatted(
eta.toHours(),
eta.toMinutesPart(),
eta.toSecondsPart()
);
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0ETA: ", TextColor.WHITE, etaString));
}
}
}
@ -98,7 +112,7 @@ public class CommandHelper {
if (plugin.checkPausedByPlayerCount()) {
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ",
Text.of(TextColor.GOLD, "paused")));
lines.add(Text.of(TextColor.GRAY, TextFormat.ITALIC, " (there are " + plugin.getConfigs().getPluginConfig().getPlayerRenderLimit() + " or more players online)"));
lines.add(Text.of(TextColor.GRAY, TextFormat.ITALIC, "\u00A0\u00A0\u00A0(there are " + plugin.getBlueMap().getConfig().getPluginConfig().getPlayerRenderLimit() + " or more players online)"));
} else {
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ",
Text.of(TextColor.RED, "stopped")
@ -126,20 +140,22 @@ public class CommandHelper {
public Text worldHelperHover() {
StringJoiner joiner = new StringJoiner("\n");
for (World world : plugin.getWorlds().values()) {
joiner.add(world.getName());
for (String worldId : plugin.getBlueMap().getWorlds().keySet()) {
joiner.add(worldId);
}
return Text.of("world").setHoverText(Text.of(TextColor.WHITE, "Available worlds: \n", TextColor.GRAY, joiner.toString()));
return Text.of(TextFormat.UNDERLINED, "world")
.setHoverText(Text.of(TextColor.WHITE, "Available worlds: \n", TextColor.GRAY, joiner.toString()));
}
public Text mapHelperHover() {
StringJoiner joiner = new StringJoiner("\n");
for (String mapId : plugin.getMaps().keySet()) {
for (String mapId : plugin.getBlueMap().getMaps().keySet()) {
joiner.add(mapId);
}
return Text.of("map").setHoverText(Text.of(TextColor.WHITE, "Available maps: \n", TextColor.GRAY, joiner.toString()));
return Text.of(TextFormat.UNDERLINED, "map")
.setHoverText(Text.of(TextColor.WHITE, "Available maps: \n", TextColor.GRAY, joiner.toString()));
}
public synchronized Optional<RenderTask> getTaskForRef(String ref) {
@ -176,4 +192,9 @@ public class CommandHelper {
return ref.subSequence(0, 4).toString();
}
public String formatTime(long timestamp) {
if (timestamp < 0) return "-";
return TIME_FORMAT.format(Instant.ofEpochMilli(timestamp));
}
}

View File

@ -24,6 +24,7 @@
*/
package de.bluecolored.bluemap.common.plugin.commands;
import com.flowpowered.math.vector.Vector2d;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3d;
import com.flowpowered.math.vector.Vector3i;
@ -38,38 +39,38 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.tree.LiteralCommandNode;
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.common.plugin.PluginState;
import de.bluecolored.bluemap.common.plugin.text.Text;
import de.bluecolored.bluemap.common.plugin.text.TextColor;
import de.bluecolored.bluemap.common.plugin.text.TextFormat;
import de.bluecolored.bluemap.common.rendermanager.MapPurgeTask;
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderTask;
import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask;
import de.bluecolored.bluemap.common.rendermanager.*;
import de.bluecolored.bluemap.common.serverinterface.CommandSource;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.debug.StateDumper;
import de.bluecolored.bluemap.common.debug.StateDumper;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.map.MapRenderState;
import de.bluecolored.bluemap.core.world.Block;
import de.bluecolored.bluemap.core.map.renderstate.TileInfoRegion;
import de.bluecolored.bluemap.core.map.renderstate.TileState;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.block.Block;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class Commands<S> {
public static final String DEFAULT_MARKER_SET_ID = "markers";
private final Plugin plugin;
private final CommandDispatcher<S> dispatcher;
private final Function<S, CommandSource> commandSourceInterface;
@ -88,26 +89,22 @@ public class Commands<S> {
public void init() {
// commands
LiteralCommandNode<S> baseCommand =
literal("bluemap")
LiteralCommandNode<S> baseCommand = literal("bluemap")
.requires(requirementsUnloaded("bluemap.status"))
.executes(this::statusCommand)
.build();
LiteralCommandNode<S> versionCommand =
literal("version")
LiteralCommandNode<S> versionCommand = literal("version")
.requires(requirementsUnloaded("bluemap.version"))
.executes(this::versionCommand)
.build();
LiteralCommandNode<S> helpCommand =
literal("help")
LiteralCommandNode<S> helpCommand = literal("help")
.requires(requirementsUnloaded("bluemap.help"))
.executes(this::helpCommand)
.build();
LiteralCommandNode<S> reloadCommand =
literal("reload")
LiteralCommandNode<S> reloadCommand = literal("reload")
.requires(requirementsUnloaded("bluemap.reload"))
.executes(context -> this.reloadCommand(context, false))
@ -116,8 +113,7 @@ public class Commands<S> {
.build();
LiteralCommandNode<S> debugCommand =
literal("debug")
LiteralCommandNode<S> debugCommand = literal("debug")
.requires(requirementsUnloaded("bluemap.debug"))
.then(literal("block")
@ -130,6 +126,15 @@ public class Commands<S> {
.then(argument("z", DoubleArgumentType.doubleArg())
.executes(this::debugBlockCommand))))))
.then(literal("map")
.requires(requirements("bluemap.debug"))
.then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin))
.executes(this::debugMapCommand)
.then(argument("x", IntegerArgumentType.integer())
.then(argument("z", IntegerArgumentType.integer())
.executes(this::debugMapCommand)))))
.then(literal("flush")
.requires(requirements("bluemap.debug"))
.executes(this::debugFlushCommand)
@ -147,31 +152,27 @@ public class Commands<S> {
.build();
LiteralCommandNode<S> stopCommand =
literal("stop")
LiteralCommandNode<S> stopCommand = literal("stop")
.requires(requirements("bluemap.stop"))
.executes(this::stopCommand)
.build();
LiteralCommandNode<S> startCommand =
literal("start")
LiteralCommandNode<S> startCommand = literal("start")
.requires(requirements("bluemap.start"))
.executes(this::startCommand)
.build();
LiteralCommandNode<S> freezeCommand =
literal("freeze")
LiteralCommandNode<S> freezeCommand = literal("freeze")
.requires(requirements("bluemap.freeze"))
.then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin))
.executes(this::freezeCommand))
.build();
LiteralCommandNode<S> unfreezeCommand =
literal("unfreeze")
.requires(requirements("bluemap.freeze"))
.then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin))
.executes(this::unfreezeCommand))
.build();
LiteralCommandNode<S> unfreezeCommand = literal("unfreeze")
.requires(requirements("bluemap.freeze"))
.then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin))
.executes(this::unfreezeCommand))
.build();
LiteralCommandNode<S> forceUpdateCommand =
addRenderArguments(
@ -180,6 +181,13 @@ public class Commands<S> {
this::forceUpdateCommand
).build();
LiteralCommandNode<S> fixEdgesCommand =
addRenderArguments(
literal("fix-edges")
.requires(requirements("bluemap.update.force")),
this::fixEdgesCommand
).build();
LiteralCommandNode<S> updateCommand =
addRenderArguments(
literal("update")
@ -187,33 +195,43 @@ public class Commands<S> {
this::updateCommand
).build();
LiteralCommandNode<S> purgeCommand =
literal("purge")
.requires(requirements("bluemap.purge"))
.then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin))
.executes(this::purgeCommand))
.build();
LiteralCommandNode<S> purgeCommand = literal("purge")
.requires(requirements("bluemap.purge"))
.then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin))
.executes(this::purgeCommand))
.build();
LiteralCommandNode<S> cancelCommand =
literal("cancel")
LiteralCommandNode<S> cancelCommand = literal("cancel")
.requires(requirements("bluemap.cancel"))
.executes(this::cancelCommand)
.then(argument("task-ref", StringArgumentType.string()).suggests(new TaskRefSuggestionProvider<>(helper))
.executes(this::cancelCommand))
.build();
LiteralCommandNode<S> worldsCommand =
literal("worlds")
LiteralCommandNode<S> worldsCommand = literal("worlds")
.requires(requirements("bluemap.status"))
.executes(this::worldsCommand)
.build();
LiteralCommandNode<S> mapsCommand =
literal("maps")
LiteralCommandNode<S> mapsCommand = literal("maps")
.requires(requirements("bluemap.status"))
.executes(this::mapsCommand)
.build();
LiteralCommandNode<S> storagesCommand = literal("storages")
.requires(requirements("bluemap.status"))
.executes(this::storagesCommand)
.then(argument("storage", StringArgumentType.string()).suggests(new StorageSuggestionProvider<>(plugin))
.executes(this::storagesInfoCommand)
.then(literal("delete")
.requires(requirements("bluemap.delete"))
.then(argument("map", StringArgumentType.string())
.executes(this::storagesDeleteMapCommand))))
.build();
// command tree
dispatcher.getRoot().addChild(baseCommand);
baseCommand.addChild(versionCommand);
@ -225,11 +243,13 @@ public class Commands<S> {
baseCommand.addChild(freezeCommand);
baseCommand.addChild(unfreezeCommand);
baseCommand.addChild(forceUpdateCommand);
baseCommand.addChild(fixEdgesCommand);
baseCommand.addChild(updateCommand);
baseCommand.addChild(cancelCommand);
baseCommand.addChild(purgeCommand);
baseCommand.addChild(worldsCommand);
baseCommand.addChild(mapsCommand);
baseCommand.addChild(storagesCommand);
}
private <B extends ArgumentBuilder<S, B>> B addRenderArguments(B builder, Command<S> command) {
@ -283,10 +303,10 @@ public class Commands<S> {
}
}
private Optional<World> parseWorld(String worldName) {
for (World world : plugin.getWorlds().values()) {
if (world.getName().equalsIgnoreCase(worldName)) {
return Optional.of(world);
private Optional<World> parseWorld(String worldId) {
for (var entry : plugin.getBlueMap().getWorlds().entrySet()) {
if (entry.getKey().equals(worldId)) {
return Optional.of(entry.getValue());
}
}
@ -294,8 +314,8 @@ public class Commands<S> {
}
private Optional<BmMap> parseMap(String mapId) {
for (BmMap map : plugin.getMaps().values()) {
if (map.getId().equalsIgnoreCase(mapId)) {
for (BmMap map : plugin.getBlueMap().getMaps().values()) {
if (map.getId().equals(mapId)) {
return Optional.of(map);
}
}
@ -314,7 +334,10 @@ public class Commands<S> {
return 0;
}
source.sendMessages(helper.createStatusMessage());
new Thread(() -> {
source.sendMessages(helper.createStatusMessage());
}, "BlueMap-Plugin-StatusCommand").start();
return 1;
}
@ -326,32 +349,27 @@ public class Commands<S> {
renderThreadCount = plugin.getRenderManager().getWorkerThreadCount();
}
MinecraftVersion minecraftVersion = plugin.getServerInterface().getMinecraftVersion();
String minecraftVersion = plugin.getServerInterface().getMinecraftVersion();
source.sendMessage(Text.of(TextFormat.BOLD, TextColor.BLUE, "Version: ", TextColor.WHITE, BlueMap.VERSION));
source.sendMessage(Text.of(TextColor.GRAY, "Commit: ", TextColor.WHITE, BlueMap.GIT_HASH));
source.sendMessage(Text.of(TextColor.GRAY, "Implementation: ", TextColor.WHITE, plugin.getImplementationType()));
source.sendMessage(Text.of(
TextColor.GRAY, "Minecraft compatibility: ", TextColor.WHITE, minecraftVersion.getVersionString(),
TextColor.GRAY, " (" + minecraftVersion.getResource().getVersion().getVersionString() + ")"
));
source.sendMessage(Text.of(TextColor.GRAY, "Minecraft: ", TextColor.WHITE, minecraftVersion));
source.sendMessage(Text.of(TextColor.GRAY, "Render-threads: ", TextColor.WHITE, renderThreadCount));
source.sendMessage(Text.of(TextColor.GRAY, "Available processors: ", TextColor.WHITE, Runtime.getRuntime().availableProcessors()));
source.sendMessage(Text.of(TextColor.GRAY, "Available memory: ", TextColor.WHITE, (Runtime.getRuntime().maxMemory() / 1024L / 1024L) + " MiB"));
if (minecraftVersion.isAtLeast(new MinecraftVersion(1, 15))) {
String clipboardValue =
"Version: " + BlueMap.VERSION + "\n" +
"Commit: " + BlueMap.GIT_HASH + "\n" +
"Implementation: " + plugin.getImplementationType() + "\n" +
"Minecraft compatibility: " + minecraftVersion.getVersionString() + " (" + minecraftVersion.getResource().getVersion().getVersionString() + ")\n" +
"Render-threads: " + renderThreadCount + "\n" +
"Available processors: " + Runtime.getRuntime().availableProcessors() + "\n" +
"Available memory: " + Runtime.getRuntime().maxMemory() / 1024L / 1024L + " MiB";
source.sendMessage(Text.of(TextColor.DARK_GRAY, "[copy to clipboard]")
.setClickAction(Text.ClickAction.COPY_TO_CLIPBOARD, clipboardValue)
.setHoverText(Text.of(TextColor.GRAY, "click to copy the above text .. ", TextFormat.ITALIC, TextColor.GRAY, "duh!")));
}
String clipboardValue =
"Version: " + BlueMap.VERSION + "\n" +
"Commit: " + BlueMap.GIT_HASH + "\n" +
"Implementation: " + plugin.getImplementationType() + "\n" +
"Minecraft: " + minecraftVersion + "\n" +
"Render-threads: " + renderThreadCount + "\n" +
"Available processors: " + Runtime.getRuntime().availableProcessors() + "\n" +
"Available memory: " + Runtime.getRuntime().maxMemory() / 1024L / 1024L + " MiB";
source.sendMessage(Text.of(TextColor.DARK_GRAY, "[copy to clipboard]")
.setClickAction(Text.ClickAction.COPY_TO_CLIPBOARD, clipboardValue)
.setHoverText(Text.of(TextColor.GRAY, "click to copy the above text .. ", TextFormat.ITALIC, TextColor.GRAY, "duh!")));
return 1;
}
@ -415,7 +433,7 @@ public class Commands<S> {
public int debugClearCacheCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
for (World world : plugin.getWorlds().values()) {
for (World world : plugin.getBlueMap().getWorlds().values()) {
world.invalidateChunkCache();
}
@ -435,7 +453,7 @@ public class Commands<S> {
world = parseWorld(worldName.get()).orElse(null);
if (world == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this name: ", TextColor.WHITE, worldName.get()));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this id: ", TextColor.WHITE, worldName.get()));
return 0;
}
} else {
@ -464,6 +482,86 @@ public class Commands<S> {
return 1;
}
public int debugMapCommand(CommandContext<S> context) {
final CommandSource source = commandSourceInterface.apply(context.getSource());
// parse arguments
String mapId = context.getArgument("map", String.class);
Optional<Integer> x = getOptionalArgument(context, "x", Integer.class);
Optional<Integer> z = getOptionalArgument(context, "z", Integer.class);
final BmMap map = parseMap(mapId).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this id: ", TextColor.WHITE, mapId));
return 0;
}
final Vector2i position;
if (x.isPresent() && z.isPresent()) {
position = new Vector2i(x.get(), z.get());
} else {
position = source.getPosition()
.map(v -> v.toVector2(true))
.map(Vector2d::floor)
.map(Vector2d::toInt)
.orElse(null);
if (position == null) {
source.sendMessage(Text.of(TextColor.RED, "Can't detect a location from this command-source, you'll have to define a position!"));
return 0;
}
}
new Thread(() -> {
// collect and output debug info
Grid chunkGrid = map.getWorld().getChunkGrid();
Grid regionGrid = map.getWorld().getRegionGrid();
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Vector2i regionPos = regionGrid.getCell(position);
Vector2i chunkPos = chunkGrid.getCell(position);
Vector2i tilePos = tileGrid.getCell(position);
TileInfoRegion.TileInfo tileInfo = map.getMapTileState().get(tilePos.getX(), tilePos.getY());
int lastChunkHash = map.getMapChunkState().get(chunkPos.getX(), chunkPos.getY());
int currentChunkHash = 0;
class FindHashConsumer implements ChunkConsumer.ListOnly {
public int timestamp = 0;
@Override
public void accept(int chunkX, int chunkZ, int timestamp) {
if (chunkPos.getX() == chunkX && chunkPos.getY() == chunkZ)
this.timestamp = timestamp;
}
}
try {
FindHashConsumer findHashConsumer = new FindHashConsumer();
map.getWorld().getRegion(regionPos.getX(), regionPos.getY())
.iterateAllChunks(findHashConsumer);
currentChunkHash = findHashConsumer.timestamp;
} catch (IOException e) {
Logger.global.logError("Failed to load chunk-hash.", e);
}
Map<String, Object> lines = new LinkedHashMap<>();
lines.put("region-pos", regionPos);
lines.put("chunk-pos", chunkPos);
lines.put("chunk-curr-hash", currentChunkHash);
lines.put("chunk-last-hash", lastChunkHash);
lines.put("tile-pos", tilePos);
lines.put("tile-render-time", tileInfo.getRenderTime());
lines.put("tile-state", tileInfo.getState().getKey().getFormatted());
source.sendMessage(Text.of(TextColor.GOLD, "Map tile info:"));
source.sendMessage(formatMap(lines));
}, "BlueMap-Plugin-DebugMapCommand").start();
return 1;
}
public int debugBlockCommand(CommandContext<S> context) {
final CommandSource source = commandSourceInterface.apply(context.getSource());
@ -481,7 +579,7 @@ public class Commands<S> {
position = new Vector3d(x.get(), y.get(), z.get());
if (world == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this name: ", TextColor.WHITE, worldName.get()));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this id: ", TextColor.WHITE, worldName.get()));
return 0;
}
} else {
@ -498,31 +596,55 @@ public class Commands<S> {
// collect and output debug info
Vector3i blockPos = position.floor().toInt();
Block<?> block = new Block<>(world, blockPos.getX(), blockPos.getY(), blockPos.getZ());
Block<?> blockBelow = new Block<>(null, 0, 0, 0).copy(block, 0, -1, 0);
// populate lazy-loaded values
block.getBlockState();
block.getBiomeId();
block.getLightData();
blockBelow.getBlockState();
blockBelow.getBiomeId();
blockBelow.getLightData();
Block<?> blockBelow = new Block<>(world, blockPos.getX(), blockPos.getY() - 1, blockPos.getZ());
source.sendMessages(Arrays.asList(
Text.of(TextColor.GOLD, "Block at you: ", TextColor.WHITE, block),
Text.of(TextColor.GOLD, "Block below you: ", TextColor.WHITE, blockBelow)
Text.of(TextColor.GOLD, "Block at you: \n", formatBlock(block)),
Text.of(TextColor.GOLD, "Block below you: \n", formatBlock(blockBelow))
));
}, "BlueMap-Plugin-DebugBlockCommand").start();
return 1;
}
private Text formatBlock(Block<?> block) {
World world = block.getWorld();
Chunk chunk = block.getChunk();
Map<String, Object> lines = new LinkedHashMap<>();
lines.put("world-id", world.getId());
lines.put("world-name", world.getName());
lines.put("chunk-is-generated", chunk.isGenerated());
lines.put("chunk-has-lightdata", chunk.hasLightData());
lines.put("chunk-inhabited-time", chunk.getInhabitedTime());
lines.put("block-state", block.getBlockState());
lines.put("biome", block.getBiome().getKey());
lines.put("position", block.getX() + " | " + block.getY() + " | " + block.getZ());
lines.put("block-light", block.getBlockLightLevel());
lines.put("sun-light", block.getSunLightLevel());
BlockEntity blockEntity = block.getBlockEntity();
if (blockEntity != null) {
lines.put("block-entity", blockEntity);
}
return formatMap(lines);
}
private Text formatMap(Map<String, Object> lines) {
Object[] textElements = lines.entrySet().stream()
.flatMap(e -> Stream.of(TextColor.GRAY, e.getKey(), ": ", TextColor.WHITE, e.getValue(), "\n"))
.toArray(Object[]::new);
textElements[textElements.length - 1] = "";
return Text.of(textElements);
}
public int debugDumpCommand(CommandContext<S> context) {
final CommandSource source = commandSourceInterface.apply(context.getSource());
try {
Path file = plugin.getConfigs().getCoreConfig().getData().resolve("dump.json");
Path file = plugin.getBlueMap().getConfig().getCoreConfig().getData().resolve("dump.json.gz");
StateDumper.global().dump(file);
source.sendMessage(Text.of(TextColor.GREEN, "Dump created at: " + file));
@ -561,7 +683,7 @@ public class Commands<S> {
new Thread(() -> {
plugin.getPluginState().setRenderThreadsEnabled(true);
plugin.getRenderManager().start(plugin.getConfigs().getCoreConfig().resolveRenderThreadCount());
plugin.getRenderManager().start(plugin.getBlueMap().getConfig().getCoreConfig().resolveRenderThreadCount());
source.sendMessage(Text.of(TextColor.GREEN, "Render-Threads started!"));
plugin.save();
@ -582,7 +704,7 @@ public class Commands<S> {
BmMap map = parseMap(mapString).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this id: ", TextColor.WHITE, mapString));
return 0;
}
@ -623,7 +745,7 @@ public class Commands<S> {
BmMap map = parseMap(mapString).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this id: ", TextColor.WHITE, mapString));
return 0;
}
@ -648,14 +770,18 @@ public class Commands<S> {
}
public int forceUpdateCommand(CommandContext<S> context) {
return updateCommand(context, true);
return updateCommand(context, s -> true);
}
public int fixEdgesCommand(CommandContext<S> context) {
return updateCommand(context, s -> s == TileState.RENDERED_EDGE);
}
public int updateCommand(CommandContext<S> context) {
return updateCommand(context, false);
return updateCommand(context, s -> false);
}
public int updateCommand(CommandContext<S> context, boolean force) {
public int updateCommand(CommandContext<S> context, Predicate<TileState> force) {
final CommandSource source = commandSourceInterface.apply(context.getSource());
// parse world/map argument
@ -670,7 +796,8 @@ public class Commands<S> {
mapToRender = parseMap(worldOrMap.get()).orElse(null);
if (mapToRender == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " or ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, worldOrMap.get()));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " or ",
helper.mapHelperHover(), " with this id: ", TextColor.WHITE, worldOrMap.get()));
return 0;
}
} else {
@ -681,7 +808,7 @@ public class Commands<S> {
mapToRender = null;
if (worldToRender == null) {
source.sendMessage(Text.of(TextColor.RED, "Can't detect a world from this command-source, you'll have to define a world or a map to update!").setHoverText(Text.of(TextColor.GRAY, "/bluemap " + (force ? "force-update" : "update") + " <world|map>")));
source.sendMessage(Text.of(TextColor.RED, "Can't detect a world from this command-source, you'll have to define a world or a map to update!"));
return 0;
}
}
@ -698,7 +825,7 @@ public class Commands<S> {
} else {
Vector3d position = source.getPosition().orElse(null);
if (position == null) {
source.sendMessage(Text.of(TextColor.RED, "Can't detect a position from this command-source, you'll have to define x,z coordinates to update with a radius!").setHoverText(Text.of(TextColor.GRAY, "/bluemap " + (force ? "force-update" : "update") + " <x> <z> " + radius)));
source.sendMessage(Text.of(TextColor.RED, "Can't detect a position from this command-source, you'll have to define x,z coordinates to update with a radius!"));
return 0;
}
@ -713,16 +840,12 @@ public class Commands<S> {
try {
List<BmMap> maps = new ArrayList<>();
if (worldToRender != null) {
var world = plugin.getServerInterface().getWorld(worldToRender.getSaveFolder()).orElse(null);
if (world != null) world.persistWorldChanges();
for (BmMap map : plugin.getMaps().values()) {
if (map.getWorld().getSaveFolder().equals(worldToRender.getSaveFolder())) maps.add(map);
plugin.flushWorldUpdates(worldToRender);
for (BmMap map : plugin.getBlueMap().getMaps().values()) {
if (map.getWorld().equals(worldToRender)) maps.add(map);
}
} else {
var world = plugin.getServerInterface().getWorld(mapToRender.getWorld().getSaveFolder()).orElse(null);
if (world != null) world.persistWorldChanges();
plugin.flushWorldUpdates(mapToRender.getWorld());
maps.add(mapToRender);
}
@ -732,15 +855,11 @@ public class Commands<S> {
}
for (BmMap map : maps) {
MapUpdateTask updateTask = new MapUpdateTask(map, center, radius);
MapUpdateTask updateTask = new MapUpdateTask(map, center, radius, force);
plugin.getRenderManager().scheduleRenderTask(updateTask);
if (force) {
MapRenderState state = map.getRenderState();
updateTask.getRegions().forEach(region -> state.setRenderTime(region, -1));
}
source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + map.getId() + "' ", TextColor.GRAY, "(" + updateTask.getRegions().size() + " regions, ~" + updateTask.getRegions().size() * 1024L + " chunks)"));
source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + map.getId() + "' ",
TextColor.GRAY, "(" + updateTask.getRegions().size() + " regions, ~" + updateTask.getRegions().size() * 1024L + " chunks)"));
}
source.sendMessage(Text.of(TextColor.GREEN, "Use ", TextColor.GRAY, "/bluemap", TextColor.GREEN, " to see the progress."));
@ -789,7 +908,7 @@ public class Commands<S> {
BmMap map = parseMap(mapString).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this id: ", TextColor.WHITE, mapString));
return 0;
}
@ -830,36 +949,131 @@ public class Commands<S> {
CommandSource source = commandSourceInterface.apply(context.getSource());
source.sendMessage(Text.of(TextColor.BLUE, "Worlds loaded by BlueMap:"));
for (var entry : plugin.getWorlds().entrySet()) {
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, entry.getValue().getName()).setHoverText(Text.of(entry.getValue().getSaveFolder(), TextColor.GRAY, " (" + entry.getKey() + ")")));
for (var entry : plugin.getBlueMap().getWorlds().entrySet()) {
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, entry.getKey()));
}
return 1;
}
public int mapsCommand(CommandContext<S> context) {
List<Text> lines = new ArrayList<>();
lines.add(Text.of(TextColor.BLUE, "Maps loaded by BlueMap:"));
for (BmMap map : plugin.getBlueMap().getMaps().values()) {
boolean frozen = !plugin.getPluginState().getMapState(map).isUpdateEnabled();
lines.add(Text.of(TextColor.GRAY, " - ",
TextColor.WHITE, map.getId(),
TextColor.GRAY, " (" + map.getName() + ")"));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0World: ",
TextColor.DARK_GRAY, map.getWorld().getId()));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0Last Update: ",
TextColor.DARK_GRAY, helper.formatTime(map.getMapTileState().getLastRenderTime() * 1000L)));
if (frozen)
lines.add(Text.of(TextColor.AQUA, TextFormat.ITALIC, "\u00A0\u00A0\u00A0This map is frozen!"));
}
CommandSource source = commandSourceInterface.apply(context.getSource());
source.sendMessages(lines);
return 1;
}
public int storagesCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
source.sendMessage(Text.of(TextColor.BLUE, "Maps loaded by BlueMap:"));
for (BmMap map : plugin.getMaps().values()) {
boolean unfrozen = plugin.getPluginState().getMapState(map).isUpdateEnabled();
if (unfrozen) {
source.sendMessage(Text.of(
TextColor.GRAY, " - ",
TextColor.WHITE, map.getId(),
TextColor.GRAY, " (" + map.getName() + ")"
).setHoverText(Text.of(TextColor.WHITE, "World: ", TextColor.GRAY, map.getWorld().getName())));
} else {
source.sendMessage(Text.of(
TextColor.GRAY, " - ",
TextColor.WHITE, map.getId(),
TextColor.GRAY, " (" + map.getName() + ") - ",
TextColor.AQUA, TextFormat.ITALIC, "frozen!"
).setHoverText(Text.of(TextColor.WHITE, "World: ", TextColor.GRAY, map.getWorld().getName())));
source.sendMessage(Text.of(TextColor.BLUE, "Storages loaded by BlueMap:"));
for (var entry : plugin.getBlueMap().getConfig().getStorageConfigs().entrySet()) {
String storageTypeKey = "?";
try {
storageTypeKey = entry.getValue().getStorageType().getKey().getFormatted();
} catch (ConfigurationException ignore) {} // should never happen
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, entry.getKey())
.setHoverText(Text.of(storageTypeKey))
.setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap storages " + entry.getKey())
);
}
return 1;
}
public int storagesInfoCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
String storageId = context.getArgument("storage", String.class);
Storage storage;
try {
storage = plugin.getBlueMap().getOrLoadStorage(storageId);
} catch (ConfigurationException | InterruptedException ex) {
Logger.global.logError("Unexpected exception trying to load storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to load this storage. Please check the console for more details..."));
return 0;
}
Collection<String> mapIds;
try {
mapIds = storage.mapIds().toList();
} catch (IOException ex) {
Logger.global.logError("Unexpected exception trying to load mapIds from storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to access this storage. Please check the console for more details..."));
return 0;
}
source.sendMessage(Text.of(TextColor.BLUE, "Storage '", storageId, "':"));
if (mapIds.isEmpty()) {
source.sendMessage(Text.of(TextColor.GRAY, " <empty storage>"));
} else {
for (String mapId : mapIds) {
BmMap map = plugin.getBlueMap().getMaps().get(mapId);
boolean isLoaded = map != null && map.getStorage().equals(storage.map(mapId));
if (isLoaded) {
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, mapId, TextColor.GREEN, TextFormat.ITALIC, " (loaded)"));
} else {
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, mapId, TextColor.DARK_GRAY, TextFormat.ITALIC, " (unloaded/static/remote)"));
}
}
}
return 1;
}
public int storagesDeleteMapCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
String storageId = context.getArgument("storage", String.class);
String mapId = context.getArgument("map", String.class);
MapStorage storage;
try {
storage = plugin.getBlueMap().getOrLoadStorage(storageId).map(mapId);
} catch (ConfigurationException | InterruptedException ex) {
Logger.global.logError("Unexpected exception trying to load storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to load this storage. Please check the console for more details..."));
return 0;
}
BmMap map = plugin.getBlueMap().getMaps().get(mapId);
boolean isLoaded = map != null && map.getStorage().equals(storage);
if (isLoaded) {
Text purgeCommand = Text.of(TextColor.WHITE, "/bluemap purge " + mapId)
.setClickAction(Text.ClickAction.SUGGEST_COMMAND, "/bluemap purge " + mapId);
source.sendMessage(Text.of(TextColor.RED, "Can't delete a loaded map!\n" +
"Unload the map by removing its config-file first,\n" +
"or use ", purgeCommand, " if you want to purge it."));
return 0;
}
// delete map
StorageDeleteTask deleteTask = new StorageDeleteTask(storage, mapId);
plugin.getRenderManager().scheduleRenderTaskNext(deleteTask);
source.sendMessage(Text.of(TextColor.GREEN, "Created new Task to delete map '" + mapId + "' from storage '" + storageId + "'"));
return 1;
}
}

View File

@ -25,7 +25,6 @@
package de.bluecolored.bluemap.common.plugin.commands;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.map.BmMap;
import java.util.Collection;
import java.util.HashSet;
@ -40,7 +39,7 @@ public class MapSuggestionProvider<S> extends AbstractSuggestionProvider<S> {
@Override
public Collection<String> getPossibleValues() {
return new HashSet<>(plugin.getMaps().keySet());
return new HashSet<>(plugin.getBlueMap().getMaps().keySet());
}
}

View File

@ -22,30 +22,23 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.serverinterface;
package de.bluecolored.bluemap.common.plugin.commands;
import java.nio.file.Path;
import de.bluecolored.bluemap.common.plugin.Plugin;
public enum Dimension {
import java.util.Collection;
OVERWORLD ("Overworld", Path.of("")),
NETHER ("Nether", Path.of("DIM-1")),
END ("End", Path.of("DIM1"));
public class StorageSuggestionProvider<S> extends AbstractSuggestionProvider<S> {
private final String name;
private final Path dimensionSubPath;
private final Plugin plugin;
Dimension(String name, Path dimensionSubPath) {
this.name = name;
this.dimensionSubPath = dimensionSubPath;
public StorageSuggestionProvider(Plugin plugin) {
this.plugin = plugin;
}
public String getName() {
return name;
}
public Path getDimensionSubPath() {
return dimensionSubPath;
@Override
public Collection<String> getPossibleValues() {
return plugin.getBlueMap().getConfig().getStorageConfigs().keySet();
}
}

View File

@ -28,7 +28,7 @@ import java.util.Collection;
public class TaskRefSuggestionProvider<S> extends AbstractSuggestionProvider<S> {
private CommandHelper helper;
private final CommandHelper helper;
public TaskRefSuggestionProvider(CommandHelper helper) {
this.helper = helper;

View File

@ -24,16 +24,14 @@
*/
package de.bluecolored.bluemap.common.plugin.commands;
import de.bluecolored.bluemap.common.plugin.Plugin;
import java.util.Collection;
import java.util.HashSet;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.world.World;
public class WorldOrMapSuggestionProvider<S> extends AbstractSuggestionProvider<S> {
private Plugin plugin;
private final Plugin plugin;
public WorldOrMapSuggestionProvider(Plugin plugin) {
this.plugin = plugin;
@ -42,13 +40,8 @@ public class WorldOrMapSuggestionProvider<S> extends AbstractSuggestionProvider<
@Override
public Collection<String> getPossibleValues() {
Collection<String> values = new HashSet<>();
for (World world : plugin.getWorlds().values()) {
values.add(world.getName());
}
values.addAll(plugin.getMaps().keySet());
values.addAll(plugin.getBlueMap().getWorlds().keySet());
values.addAll(plugin.getBlueMap().getMaps().keySet());
return values;
}

View File

@ -25,7 +25,6 @@
package de.bluecolored.bluemap.common.plugin.commands;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.world.World;
import java.util.Collection;
import java.util.HashSet;
@ -40,13 +39,7 @@ public class WorldSuggestionProvider<S> extends AbstractSuggestionProvider<S> {
@Override
public Collection<String> getPossibleValues() {
Collection<String> values = new HashSet<>();
for (World world : plugin.getWorlds().values()) {
values.add(world.getName());
}
return values;
return new HashSet<>(plugin.getBlueMap().getWorlds().keySet());
}
}

View File

@ -1,3 +1,27 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.plugin.skins;
import de.bluecolored.bluemap.api.plugin.PlayerIconFactory;

View File

@ -1,3 +1,27 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.plugin.skins;
import com.google.gson.JsonArray;

View File

@ -24,7 +24,6 @@
*/
package de.bluecolored.bluemap.common.plugin.skins;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.api.plugin.PlayerIconFactory;
import de.bluecolored.bluemap.api.plugin.SkinProvider;
import de.bluecolored.bluemap.common.plugin.Plugin;
@ -45,7 +44,6 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@DebugDump
public class PlayerSkinUpdater implements ServerEventListener {
private final Plugin plugin;
@ -83,7 +81,7 @@ public class PlayerSkinUpdater implements ServerEventListener {
return;
}
Map<String, BmMap> maps = plugin.getMaps();
Map<String, BmMap> maps = plugin.getBlueMap().getMaps();
if (maps == null) {
Logger.global.logDebug("Could not update skin, since the plugin seems not to be ready.");
return;
@ -92,7 +90,7 @@ public class PlayerSkinUpdater implements ServerEventListener {
BufferedImage playerHead = playerMarkerIconFactory.apply(playerUuid, skin.get());
for (BmMap map : maps.values()) {
try (OutputStream out = map.getStorage().writeMeta(map.getId(), "assets/playerheads/" + playerUuid + ".png")) {
try (OutputStream out = map.getStorage().asset("playerheads/" + playerUuid + ".png").write()) {
ImageIO.write(playerHead, "png", out);
} catch (IOException ex) {
Logger.global.logError("Failed to write player skin to storage: " + playerUuid, ex);

View File

@ -24,11 +24,8 @@
*/
package de.bluecolored.bluemap.common.rendermanager;
import de.bluecolored.bluemap.api.debug.DebugDump;
import java.util.*;
@DebugDump
public class CombinedRenderTask<T extends RenderTask> implements RenderTask {
private final String description;
@ -65,11 +62,12 @@ public class CombinedRenderTask<T extends RenderTask> implements RenderTask {
}
@Override
public synchronized double estimateProgress() {
if (!hasMoreWork()) return 1;
public double estimateProgress() {
int currentTask = this.currentTaskIndex;
if (currentTask >= this.tasks.size()) return 1;
double total = currentTaskIndex;
total += this.tasks.get(this.currentTaskIndex).estimateProgress();
double total = currentTask;
total += this.tasks.get(currentTask).estimateProgress();
return total / tasks.size();
}
@ -83,13 +81,10 @@ public class CombinedRenderTask<T extends RenderTask> implements RenderTask {
public boolean contains(RenderTask task) {
if (this.equals(task)) return true;
if (task instanceof CombinedRenderTask) {
CombinedRenderTask<?> combinedTask = (CombinedRenderTask<?>) task;
if (task instanceof CombinedRenderTask<?> combinedTask) {
for (RenderTask subTask : combinedTask.tasks) {
if (!this.contains(subTask)) return false;
}
return true;
}
@ -110,4 +105,5 @@ public class CombinedRenderTask<T extends RenderTask> implements RenderTask {
if (this.currentTaskIndex >= this.tasks.size()) return Optional.empty();
return Optional.ofNullable(this.tasks.get(this.currentTaskIndex).getDescription());
}
}

View File

@ -24,7 +24,7 @@
*/
package de.bluecolored.bluemap.common.rendermanager;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.debug.DebugDump;
import de.bluecolored.bluemap.core.map.BmMap;
import java.util.Objects;
@ -50,23 +50,20 @@ public class MapPurgeTask implements RenderTask {
if (!this.hasMoreWork) return;
this.hasMoreWork = false;
}
if (this.cancelled) return;
// save lowres-tile-manager to clear/flush any buffered data
this.map.getLowresTileManager().save();
try {
// purge the map
map.getStorage().purgeMap(map.getId(), progressInfo -> {
this.progress = progressInfo.getProgress();
return !this.cancelled;
});
// purge the map
map.getStorage().delete(progress -> {
this.progress = progress;
return !this.cancelled;
});
// reset texture gallery
map.resetTextureGallery();
} finally {
// reset renderstate
map.getRenderState().reset();
}
map.resetTextureGallery();
map.getMapTileState().reset();
map.getMapChunkState().reset();
}
@Override
@ -97,7 +94,7 @@ public class MapPurgeTask implements RenderTask {
@Override
public String getDescription() {
return "Purge Map " + map.getId();
return "Purge map " + map.getId();
}
}

View File

@ -25,19 +25,21 @@
package de.bluecolored.bluemap.common.rendermanager;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.map.renderstate.MapTileState;
import de.bluecolored.bluemap.core.map.renderstate.TileInfoRegion;
import de.bluecolored.bluemap.core.map.renderstate.TileState;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.io.IOException;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@DebugDump
public class MapUpdateTask extends CombinedRenderTask<RenderTask> {
private final BmMap map;
@ -47,7 +49,7 @@ public class MapUpdateTask extends CombinedRenderTask<RenderTask> {
this(map, getRegions(map));
}
public MapUpdateTask(BmMap map, boolean force) {
public MapUpdateTask(BmMap map, Predicate<TileState> force) {
this(map, getRegions(map), force);
}
@ -55,15 +57,15 @@ public class MapUpdateTask extends CombinedRenderTask<RenderTask> {
this(map, getRegions(map, center, radius));
}
public MapUpdateTask(BmMap map, Vector2i center, int radius, boolean force) {
public MapUpdateTask(BmMap map, Vector2i center, int radius, Predicate<TileState> force) {
this(map, getRegions(map, center, radius), force);
}
public MapUpdateTask(BmMap map, Collection<Vector2i> regions) {
this(map, regions, false);
this(map, regions, s -> false);
}
public MapUpdateTask(BmMap map, Collection<Vector2i> regions, boolean force) {
public MapUpdateTask(BmMap map, Collection<Vector2i> regions, Predicate<TileState> force) {
super("Update map '" + map.getId() + "'", createTasks(map, regions, force));
this.map = map;
this.regions = Collections.unmodifiableCollection(new ArrayList<>(regions));
@ -77,7 +79,7 @@ public class MapUpdateTask extends CombinedRenderTask<RenderTask> {
return regions;
}
private static Collection<RenderTask> createTasks(BmMap map, Collection<Vector2i> regions, boolean force) {
private static Collection<RenderTask> createTasks(BmMap map, Collection<Vector2i> regions, Predicate<TileState> force) {
ArrayList<WorldRegionRenderTask> regionTasks = new ArrayList<>(regions.size());
regions.forEach(region -> regionTasks.add(new WorldRegionRenderTask(map, region, force)));
@ -99,42 +101,65 @@ public class MapUpdateTask extends CombinedRenderTask<RenderTask> {
return tasks;
}
private static List<Vector2i> getRegions(BmMap map) {
private static Collection<Vector2i> getRegions(BmMap map) {
return getRegions(map, null, -1);
}
private static List<Vector2i> getRegions(BmMap map, Vector2i center, int radius) {
private static Collection<Vector2i> getRegions(BmMap map, Vector2i center, int radius) {
World world = map.getWorld();
Grid regionGrid = world.getRegionGrid();
Predicate<Vector2i> regionFilter = r -> {
Vector2i cellMin = regionGrid.getCellMin(r);
if (cellMin.getX() > map.getMapSettings().getMaxPos().getX()) return false;
if (cellMin.getY() > map.getMapSettings().getMaxPos().getZ()) return false;
Vector2i cellMax = regionGrid.getCellMax(r);
if (cellMax.getX() < map.getMapSettings().getMinPos().getX()) return false;
return cellMax.getY() >= map.getMapSettings().getMinPos().getZ();
};
Predicate<Vector2i> regionBoundsFilter = map.getMapSettings().getCellRenderBoundariesFilter(regionGrid, true);
Predicate<Vector2i> regionRadiusFilter;
if (center == null || radius < 0) {
return world.listRegions().stream()
.filter(regionFilter)
.collect(Collectors.toList());
regionRadiusFilter = r -> true;
} else {
Vector2i halfCell = regionGrid.getGridSize().div(2);
long increasedRadiusSquared = (long) Math.pow(radius + Math.ceil(halfCell.length()), 2);
regionRadiusFilter = r -> {
Vector2i min = regionGrid.getCellMin(r);
Vector2i regionCenter = min.add(halfCell);
return regionCenter.toLong().distanceSquared(center.toLong()) <= increasedRadiusSquared;
};
}
List<Vector2i> regions = new ArrayList<>();
Vector2i halfCell = regionGrid.getGridSize().div(2);
long increasedRadiusSquared = (long) Math.pow(radius + Math.ceil(halfCell.length()), 2);
Set<Vector2i> regions = new HashSet<>();
for (Vector2i region : world.listRegions()) {
if (!regionFilter.test(region)) continue;
// update all regions in the world-files
world.listRegions().stream()
.filter(regionBoundsFilter)
.filter(regionRadiusFilter)
.forEach(regions::add);
Vector2i min = regionGrid.getCellMin(region);
Vector2i regionCenter = min.add(halfCell);
if (regionCenter.toLong().distanceSquared(center.toLong()) <= increasedRadiusSquared)
regions.add(region);
// also update regions that are present as map-tile-state files (they might have been rendered before but deleted now)
// (a little hacky as we are operating on raw tile-state files -> maybe find a better way?)
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid cellGrid = MapTileState.GRID.multiply(tileGrid);
try (Stream<GridStorage.Cell> stream = map.getStorage().tileState().stream()) {
stream
.filter(c -> {
// filter out files that are fully UNKNOWN/NOT_GENERATED
// this avoids unnecessarily converting UNKNOWN tiles into NOT_GENERATED tiles on force-updates
try (CompressedInputStream in = c.read()) {
if (in == null) return false;
TileState[] states = TileInfoRegion.loadPalette(in.decompress());
for (TileState state : states) {
if (
state != TileState.UNKNOWN &&
state != TileState.NOT_GENERATED
) return true;
}
return false;
} catch (IOException ignore) {
return true;
}
})
.map(c -> new Vector2i(c.getX(), c.getZ()))
.flatMap(v -> cellGrid.getIntersecting(v, regionGrid).stream())
.filter(regionRadiusFilter)
.forEach(regions::add);
} catch (IOException ex) {
Logger.global.logError("Failed to load map tile state!", ex);
}
return regions;

View File

@ -24,7 +24,6 @@
*/
package de.bluecolored.bluemap.common.rendermanager;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import java.util.*;
@ -35,17 +34,19 @@ import java.util.function.Predicate;
public class RenderManager {
private static final AtomicInteger nextRenderManagerIndex = new AtomicInteger(0);
@DebugDump private final int id;
@DebugDump private volatile boolean running;
private final int id;
private volatile boolean running;
private long lastTimeBusy;
private final AtomicInteger nextWorkerThreadIndex;
@DebugDump private final Collection<WorkerThread> workerThreads;
private final Collection<WorkerThread> workerThreads;
private final AtomicInteger busyCount;
private ProgressTracker progressTracker;
private volatile boolean newTask;
@DebugDump private final LinkedList<RenderTask> renderTasks;
private final LinkedList<RenderTask> renderTasks;
public RenderManager() {
this.id = nextRenderManagerIndex.getAndIncrement();
@ -55,6 +56,8 @@ public class RenderManager {
this.workerThreads = new ConcurrentLinkedDeque<>();
this.busyCount = new AtomicInteger(0);
this.lastTimeBusy = -1;
this.progressTracker = null;
this.newTask = true;
@ -102,9 +105,23 @@ public class RenderManager {
}
public void awaitIdle() throws InterruptedException {
awaitIdle(false);
}
public void awaitIdle(boolean log) throws InterruptedException {
synchronized (this.renderTasks) {
while (!this.renderTasks.isEmpty())
this.renderTasks.wait(10000);
while (!this.renderTasks.isEmpty()) {
this.renderTasks.wait(5000);
if (log) {
RenderTask task = this.getCurrentRenderTask();
if (task != null) {
Logger.global.logInfo("Waiting for task '" + task.getDescription() + "' to stop.. (" +
(Math.round(task.estimateProgress() * 10000) / 100.0) + "%)");
}
}
}
}
}
@ -249,6 +266,10 @@ public class RenderManager {
return workerThreads.size();
}
public long getLastTimeBusy() {
return lastTimeBusy;
}
private void removeTasksThatAreContainedIn(RenderTask containingTask) {
synchronized (this.renderTasks) {
if (renderTasks.size() < 2) return;
@ -290,13 +311,15 @@ public class RenderManager {
}
this.busyCount.incrementAndGet();
this.lastTimeBusy = System.currentTimeMillis();
}
try {
task.doWork();
} finally {
synchronized (renderTasks) {
this.busyCount.decrementAndGet();
int busyCount = this.busyCount.decrementAndGet();
if (busyCount > 0) this.lastTimeBusy = System.currentTimeMillis();
this.renderTasks.notifyAll();
}
}

View File

@ -0,0 +1,96 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.rendermanager;
import de.bluecolored.bluemap.common.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.MapStorage;
import java.util.Objects;
public class StorageDeleteTask implements RenderTask {
private final MapStorage storage;
private final String mapId;
private volatile double progress;
private volatile boolean hasMoreWork;
private volatile boolean cancelled;
public StorageDeleteTask(MapStorage storage, String mapId) {
this.storage = Objects.requireNonNull(storage);
this.mapId = Objects.requireNonNull(mapId);
this.progress = 0d;
this.hasMoreWork = true;
this.cancelled = false;
}
@Override
public void doWork() throws Exception {
synchronized (this) {
if (!this.hasMoreWork) return;
this.hasMoreWork = false;
}
if (this.cancelled) return;
// purge the map
storage.delete(progress -> {
this.progress = progress;
return !this.cancelled;
});
}
@Override
public boolean hasMoreWork() {
return this.hasMoreWork && !this.cancelled;
}
@Override
@DebugDump
public double estimateProgress() {
return this.progress;
}
@Override
public void cancel() {
this.cancelled = true;
}
@Override
public boolean contains(RenderTask task) {
if (task == this) return true;
if (task instanceof StorageDeleteTask) {
StorageDeleteTask sTask = (StorageDeleteTask) task;
return storage.equals(sTask.storage) && mapId.equals(sTask.mapId);
}
return false;
}
@Override
public String getDescription() {
return "Delete map " + mapId;
}
}

View File

@ -26,205 +26,248 @@ package de.bluecolored.bluemap.common.rendermanager;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector2l;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.map.renderstate.TileActionResolver.ActionAndNextState;
import de.bluecolored.bluemap.core.map.renderstate.TileActionResolver.BoundsSituation;
import de.bluecolored.bluemap.core.map.renderstate.TileInfoRegion;
import de.bluecolored.bluemap.core.map.renderstate.TileState;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.world.Region;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.io.IOException;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@DebugDump
import static de.bluecolored.bluemap.core.map.renderstate.TileActionResolver.Action.DELETE;
import static de.bluecolored.bluemap.core.map.renderstate.TileActionResolver.Action.RENDER;
public class WorldRegionRenderTask implements RenderTask {
private final BmMap map;
private final Vector2i worldRegion;
private final boolean force;
@Getter private final BmMap map;
@Getter private final Vector2i regionPos;
@Getter private final Predicate<TileState> force;
private Deque<Vector2i> tiles;
private int tileCount;
private long startTime;
private Grid regionGrid, chunkGrid, tileGrid;
private Vector2i chunkMin, chunkMax, chunksSize;
private Vector2i tileMin, tileMax, tileSize;
private int[] chunkHashes;
private ActionAndNextState[] tileActions;
private volatile int nextTileX, nextTileZ;
private volatile int atWork;
private volatile boolean cancelled;
private volatile boolean completed, cancelled;
public WorldRegionRenderTask(BmMap map, Vector2i worldRegion) {
this(map, worldRegion, false);
public WorldRegionRenderTask(BmMap map, Vector2i regionPos) {
this(map, regionPos, false);
}
public WorldRegionRenderTask(BmMap map, Vector2i worldRegion, boolean force) {
public WorldRegionRenderTask(BmMap map, Vector2i regionPos, boolean force) {
this(map, regionPos, s -> force);
}
public WorldRegionRenderTask(BmMap map, Vector2i regionPos, Predicate<TileState> force) {
this.map = map;
this.worldRegion = worldRegion;
this.regionPos = regionPos;
this.force = force;
this.tiles = null;
this.tileCount = -1;
this.startTime = -1;
this.nextTileX = 0;
this.nextTileZ = 0;
this.atWork = 0;
this.completed = false;
this.cancelled = false;
}
private synchronized void init() {
Set<Vector2l> tileSet = new HashSet<>();
startTime = System.currentTimeMillis();
//Logger.global.logInfo("Starting: " + worldRegion);
// calculate bounds
this.regionGrid = map.getWorld().getRegionGrid();
this.chunkGrid = map.getWorld().getChunkGrid();
this.tileGrid = map.getHiresModelManager().getTileGrid();
this.chunkMin = regionGrid.getCellMin(regionPos, chunkGrid);
this.chunkMax = regionGrid.getCellMax(regionPos, chunkGrid);
this.chunksSize = chunkMax.sub(chunkMin).add(1, 1);
this.tileMin = regionGrid.getCellMin(regionPos, tileGrid);
this.tileMax = regionGrid.getCellMax(regionPos, tileGrid);
this.tileSize = tileMax.sub(tileMin).add(1, 1);
long changesSince = 0;
if (!force) changesSince = map.getRenderState().getRenderTime(worldRegion);
// load chunk-hash array
int chunkMaxCount = chunksSize.getX() * chunksSize.getY();
try {
chunkHashes = new int[chunkMaxCount];
map.getWorld().getRegion(regionPos.getX(), regionPos.getY())
.iterateAllChunks( (ChunkConsumer.ListOnly) (x, z, timestamp) -> {
chunkHashes[chunkIndex(
x - chunkMin.getX(),
z - chunkMin.getY()
)] = timestamp;
map.getWorld().invalidateChunkCache(x, z);
});
} catch (IOException ex) {
Logger.global.logError("Failed to load chunks for region " + regionPos, ex);
cancel();
}
Region region = map.getWorld().getRegion(worldRegion.getX(), worldRegion.getY());
Collection<Vector2i> chunks = region.listChunks(changesSince);
// check tile actions
int tileMaxCount = tileSize.getX() * tileSize.getY();
int tileRenderCount = 0;
int tileDeleteCount = 0;
tileActions = new ActionAndNextState[tileMaxCount];
for (int x = 0; x < tileSize.getX(); x++) {
for (int z = 0; z < tileSize.getY(); z++) {
Vector2i tile = new Vector2i(tileMin.getX() + x, tileMin.getY() + z);
TileState tileState = map.getMapTileState().get(tile.getX(), tile.getY()).getState();
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid chunkGrid = map.getWorld().getChunkGrid();
int tileIndex = tileIndex(x, z);
tileActions[tileIndex] = tileState.findActionAndNextState(
force.test(tileState) || checkChunksHaveChanges(tile),
checkTileBounds(tile)
);
for (Vector2i chunk : chunks) {
Vector2i tileMin = chunkGrid.getCellMin(chunk, tileGrid);
Vector2i tileMax = chunkGrid.getCellMax(chunk, tileGrid);
for (int x = tileMin.getX(); x <= tileMax.getX(); x++) {
for (int z = tileMin.getY(); z <= tileMax.getY(); z++) {
tileSet.add(new Vector2l(x, z));
}
if (tileActions[tileIndex].action() == RENDER)
tileRenderCount++;
if (tileActions[tileIndex].action() == DELETE)
tileDeleteCount++;
}
}
Predicate<Vector2i> boundsTileFilter = t -> {
Vector2i cellMin = tileGrid.getCellMin(t);
if (cellMin.getX() > map.getMapSettings().getMaxPos().getX()) return false;
if (cellMin.getY() > map.getMapSettings().getMaxPos().getZ()) return false;
if (tileRenderCount >= tileMaxCount * 0.75)
map.getWorld().preloadRegionChunks(regionPos.getX(), regionPos.getY());
Vector2i cellMax = tileGrid.getCellMax(t);
if (cellMax.getX() < map.getMapSettings().getMinPos().getX()) return false;
return cellMax.getY() >= map.getMapSettings().getMinPos().getZ();
};
if (tileRenderCount + tileDeleteCount == 0)
completed = true;
this.tileCount = tileSet.size();
this.tiles = tileSet.stream()
.sorted(WorldRegionRenderTask::compareVec2L) //sort with longs to avoid overflow (comparison uses distanceSquared)
.map(Vector2l::toInt) // back to ints
.filter(boundsTileFilter)
.filter(map.getTileFilter())
.collect(Collectors.toCollection(ArrayDeque::new));
if (tiles.isEmpty()) complete();
}
@Override
public void doWork() {
if (cancelled) return;
if (cancelled || completed) return;
Vector2i tile;
int tileX, tileZ;
synchronized (this) {
if (tiles == null) init();
if (tiles.isEmpty()) return;
if (cancelled || completed) return;
tile = tiles.pollFirst();
tileX = nextTileX;
tileZ = nextTileZ;
if (tileX == 0 && tileZ == 0) {
init();
if (cancelled || completed) return;
}
nextTileX = tileX + 1;
if (nextTileX >= tileSize.getX()) {
nextTileZ = tileZ + 1;
nextTileX = 0;
}
if (nextTileZ >= tileSize.getY()) {
completed = true;
}
this.atWork++;
}
//Logger.global.logInfo("Working on " + worldRegion + " - Tile " + tile);
if (tileRenderPreconditions(tile)) {
map.renderTile(tile); // <- actual work
}
processTile(tileX, tileZ);
synchronized (this) {
this.atWork--;
if (atWork <= 0 && tiles.isEmpty() && !cancelled) {
if (atWork <= 0 && completed && !cancelled) {
complete();
}
}
}
private boolean tileRenderPreconditions(Vector2i tile) {
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid chunkGrid = map.getWorld().getChunkGrid();
private void processTile(int x, int z) {
Vector2i tile = new Vector2i(tileMin.getX() + x, tileMin.getY() + z);
ActionAndNextState action = tileActions[tileIndex(x, z)];
TileState resultState = TileState.RENDER_ERROR;
Vector2i minChunk = tileGrid.getCellMin(tile, chunkGrid);
Vector2i maxChunk = tileGrid.getCellMax(tile, chunkGrid);
try {
long minInhab = map.getMapSettings().getMinInhabitedTime();
int minInhabRadius = map.getMapSettings().getMinInhabitedTimeRadius();
if (minInhabRadius < 0) minInhabRadius = 0;
if (minInhabRadius > 16) minInhabRadius = 16; // sanity check
boolean isInhabited = false;
resultState = switch (action.action()) {
for (int x = minChunk.getX(); x <= maxChunk.getX(); x++) {
for (int z = minChunk.getY(); z <= maxChunk.getY(); z++) {
Chunk chunk = map.getWorld().getChunk(x, z);
if (!chunk.isGenerated()) return false;
if (chunk.getInhabitedTime() >= minInhab) isInhabited = true;
}
}
case NONE -> action.state();
if (minInhabRadius > 0 && !isInhabited) {
for (int x = minChunk.getX() - minInhabRadius; x <= maxChunk.getX() + minInhabRadius; x++) {
for (int z = minChunk.getY() - minInhabRadius; z <= maxChunk.getY() + minInhabRadius; z++) {
Chunk chunk = map.getWorld().getChunk(x, z);
if (chunk.getInhabitedTime() >= minInhab) {
isInhabited = true;
break;
case RENDER -> {
TileState failedState = checkTileRenderPreconditions(tile);
if (failedState != null){
map.unrenderTile(tile);
yield failedState;
}
map.renderTile(tile);
yield action.state();
}
}
case DELETE -> {
map.unrenderTile(tile);
yield action.state();
}
};
} catch (Exception ex) {
Logger.global.logError("Error while processing map-tile " + tile + " for map '" + map.getId() + "'", ex);
} finally {
// mark tile with new state
map.getMapTileState().set(tile.getX(), tile.getY(), new TileInfoRegion.TileInfo(
(int) (System.currentTimeMillis() / 1000),
resultState
));
}
return isInhabited;
}
private void complete() {
map.getRenderState().setRenderTime(worldRegion, startTime);
private synchronized void complete() {
// save chunk-hashes
if (chunkHashes != null) {
for (int x = 0; x < chunksSize.getX(); x++) {
for (int z = 0; z < chunksSize.getY(); z++) {
int hash = chunkHashes[chunkIndex(x, z)];
map.getMapChunkState().set(chunkMin.getX() + x, chunkMin.getY() + z, hash);
}
}
chunkHashes = null;
}
//Logger.global.logInfo("Done with: " + worldRegion);
// save map (at most, every minute)
map.save(TimeUnit.MINUTES.toMillis(1));
}
@Override
@DebugDump
public synchronized boolean hasMoreWork() {
return !cancelled && (tiles == null || !tiles.isEmpty());
return !completed && !cancelled;
}
@Override
@DebugDump
public double estimateProgress() {
if (tiles == null) return 0;
if (tileCount == 0) return 1;
double remainingTiles = tiles.size();
return 1 - (remainingTiles / this.tileCount);
if (tileSize == null) return 0;
return Math.min((double) (nextTileZ * tileSize.getX() + nextTileX) / (tileSize.getX() * tileSize.getY()), 1);
}
@Override
public void cancel() {
this.cancelled = true;
synchronized (this) {
if (tiles != null) this.tiles.clear();
}
}
public BmMap getMap() {
return map;
}
public Vector2i getWorldRegion() {
return worldRegion;
}
public boolean isForce() {
return force;
}
@Override
public String getDescription() {
return "Update region " + getWorldRegion() + " for map '" + map.getId() + "'";
return "Update region " + regionPos + " for map '" + map.getId() + "'";
}
@Override
@ -232,19 +275,101 @@ public class WorldRegionRenderTask implements RenderTask {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WorldRegionRenderTask that = (WorldRegionRenderTask) o;
return force == that.force && map.getId().equals(that.map.getId()) && worldRegion.equals(that.worldRegion);
return force == that.force && map.getId().equals(that.map.getId()) && regionPos.equals(that.regionPos);
}
@Override
public int hashCode() {
return worldRegion.hashCode();
return regionPos.hashCode();
}
private int chunkIndex(int x, int z) {
return z * chunksSize.getX() + x;
}
private int tileIndex(int x, int z) {
return z * tileSize.getX() + x;
}
private boolean checkChunksHaveChanges(Vector2i tile) {
int minX = tileGrid.getCellMinX(tile.getX(), chunkGrid),
maxX = tileGrid.getCellMaxX(tile.getX(), chunkGrid),
minZ = tileGrid.getCellMinY(tile.getY(), chunkGrid),
maxZ = tileGrid.getCellMaxY(tile.getY(), chunkGrid);
for (int chunkX = minX; chunkX <= maxX; chunkX++) {
for (int chunkZ = minZ; chunkZ <= maxZ; chunkZ++) {
int dx = chunkX - chunkMin.getX();
int dz = chunkZ - chunkMin.getY();
// only check hash for chunks inside the current region
if (
chunkX >= chunkMin.getX() && chunkX <= chunkMax.getX() &&
chunkZ >= chunkMin.getY() && chunkZ <= chunkMax.getY()
) {
int hash = chunkHashes[chunkIndex(dx, dz)];
int lastHash = map.getMapChunkState().get(chunkX, chunkZ);
if (lastHash != hash) return true;
}
}
}
return false;
}
private BoundsSituation checkTileBounds(Vector2i tile) {
boolean isInsideBounds = map.getMapSettings().isInsideRenderBoundaries(tile, tileGrid, true);
if (!isInsideBounds) return BoundsSituation.OUTSIDE;
boolean isFullyInsideBounds = map.getMapSettings().isInsideRenderBoundaries(tile, tileGrid, false);
return isFullyInsideBounds ? BoundsSituation.INSIDE : BoundsSituation.EDGE;
}
private @Nullable TileState checkTileRenderPreconditions(Vector2i tile) {
boolean chunksAreInhabited = false;
long minInhabitedTime = map.getMapSettings().getMinInhabitedTime();
int minInhabitedTimeRadius = map.getMapSettings().getMinInhabitedTimeRadius();
boolean requireLight = !map.getMapSettings().isIgnoreMissingLightData();
int minX = tileGrid.getCellMinX(tile.getX(), chunkGrid),
maxX = tileGrid.getCellMaxX(tile.getX(), chunkGrid),
minZ = tileGrid.getCellMinY(tile.getY(), chunkGrid),
maxZ = tileGrid.getCellMaxY(tile.getY(), chunkGrid);
for (int chunkX = minX; chunkX <= maxX; chunkX++) {
for (int chunkZ = minZ; chunkZ <= maxZ; chunkZ++) {
Chunk chunk = map.getWorld().getChunk(chunkX, chunkZ);
if (chunk == Chunk.ERRORED_CHUNK) return TileState.CHUNK_ERROR;
if (!chunk.isGenerated()) return TileState.NOT_GENERATED;
if (requireLight && !chunk.hasLightData()) return TileState.MISSING_LIGHT;
if (chunk.getInhabitedTime() >= minInhabitedTime) chunksAreInhabited = true;
}
}
// second pass for increased inhabited-time-radius
if (!chunksAreInhabited && minInhabitedTimeRadius > 0) {
inhabitedRadiusCheck:
for (int chunkX = minX - minInhabitedTimeRadius; chunkX <= maxX + minInhabitedTimeRadius; chunkX++) {
for (int chunkZ = minZ - minInhabitedTimeRadius; chunkZ <= maxZ + minInhabitedTimeRadius; chunkZ++) {
Chunk chunk = map.getWorld().getChunk(chunkX, chunkZ);
if (chunk.getInhabitedTime() >= minInhabitedTime) {
chunksAreInhabited = true;
break inhabitedRadiusCheck;
}
}
}
}
return chunksAreInhabited ? null : TileState.LOW_INHABITED_TIME;
}
public static Comparator<WorldRegionRenderTask> defaultComparator(final Vector2i centerRegion) {
return (task1, task2) -> {
// use long to compare to avoid overflow (comparison uses distanceSquared)
Vector2l task1Rel = new Vector2l(task1.worldRegion.getX() - centerRegion.getX(), task1.worldRegion.getY() - centerRegion.getY());
Vector2l task2Rel = new Vector2l(task2.worldRegion.getX() - centerRegion.getX(), task2.worldRegion.getY() - centerRegion.getY());
Vector2l task1Rel = new Vector2l(task1.regionPos.getX() - centerRegion.getX(), task1.regionPos.getY() - centerRegion.getY());
Vector2l task2Rel = new Vector2l(task2.regionPos.getX() - centerRegion.getX(), task2.regionPos.getY() - centerRegion.getY());
return compareVec2L(task1Rel, task2Rel);
};
}

View File

@ -35,7 +35,7 @@ public interface Player {
Text getName();
String getWorld();
ServerWorld getWorld();
Vector3d getPosition();
@ -48,8 +48,6 @@ public interface Player {
int getBlockLight();
boolean isOnline();
/**
* Return <code>true</code> if the player is sneaking.
* <p><i>If the player is offline the value of this method is undetermined.</i></p>

View File

@ -24,43 +24,20 @@
*/
package de.bluecolored.bluemap.common.serverinterface;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.debug.DebugDump;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
public interface ServerInterface {
public interface Server {
@DebugDump
MinecraftVersion getMinecraftVersion();
/**
* Registers a ServerEventListener, every method of this interface should be called on the specified events
*/
void registerListener(ServerEventListener listener);
/**
* Removes all registered listeners
*/
void unregisterAllListeners();
default Optional<ServerWorld> getWorld(Path worldFolder) {
Path normalizedWorldFolder = worldFolder.toAbsolutePath().normalize();
return getLoadedWorlds().stream()
.filter(world -> world.getSaveFolder().toAbsolutePath().normalize().equals(normalizedWorldFolder))
.findAny();
}
default Optional<ServerWorld> getWorld(Object world) {
return Optional.empty();
}
@DebugDump
Collection<ServerWorld> getLoadedWorlds();
@Nullable String getMinecraftVersion();
/**
* Returns the Folder containing the configurations for the plugin
@ -82,6 +59,40 @@ public interface ServerInterface {
return Tristate.UNDEFINED;
}
/**
* Returns the correct {@link ServerWorld} for a {@link World} if there is any.
*/
default Optional<ServerWorld> getServerWorld(World world) {
if (world instanceof MCAWorld) {
MCAWorld mcaWorld = (MCAWorld) world;
return getLoadedServerWorlds().stream()
.filter(serverWorld ->
serverWorld.getWorldFolder().toAbsolutePath().normalize()
.equals(mcaWorld.getWorldFolder().toAbsolutePath().normalize()) &&
serverWorld.getDimension().equals(mcaWorld.getDimension())
)
.findAny();
}
return Optional.empty();
}
/**
* Returns the correct {@link ServerWorld} for any Object if there is any, this should return the correct ServerWorld
* for any implementation-specific object that represent or identify a world in any way.<br>
* Used for the API implementation.
*/
default Optional<ServerWorld> getServerWorld(Object world) {
if (world instanceof World)
return getServerWorld((World) world);
return Optional.empty();
}
/**
* Returns all loaded worlds of this server.
*/
@DebugDump
Collection<ServerWorld> getLoadedServerWorlds();
/**
* Returns a collection of the states of players that are currently online
*/
@ -89,9 +100,13 @@ public interface ServerInterface {
Collection<Player> getOnlinePlayers();
/**
* Returns the state of the player with that UUID if present<br>
* this method is only guaranteed to return a {@link Player} if the player is currently online.
* Registers a ServerEventListener, every method of this interface should be called on the specified events
*/
Optional<Player> getPlayer(UUID uuid);
void registerListener(ServerEventListener listener);
/**
* Removes all registered listeners
*/
void unregisterAllListeners();
}

View File

@ -24,8 +24,6 @@
*/
package de.bluecolored.bluemap.common.serverinterface;
import de.bluecolored.bluemap.common.plugin.text.Text;
import java.util.UUID;
public interface ServerEventListener {
@ -34,6 +32,4 @@ public interface ServerEventListener {
default void onPlayerLeave(UUID playerUuid) {};
default void onChatMessage(Text message) {};
}

View File

@ -24,41 +24,24 @@
*/
package de.bluecolored.bluemap.common.serverinterface;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.debug.DebugDump;
import de.bluecolored.bluemap.core.util.Key;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
public interface ServerWorld {
@DebugDump
default Optional<String> getId() {
return Optional.empty();
}
Path getWorldFolder();
@DebugDump
default Optional<String> getName() {
return Optional.empty();
}
@DebugDump
Path getSaveFolder();
@DebugDump
default Dimension getDimension() {
Path saveFolder = getSaveFolder();
String lastName = saveFolder.getFileName().toString();
if (lastName.equals("DIM-1")) return Dimension.NETHER;
if (lastName.equals("DIM1")) return Dimension.END;
return Dimension.OVERWORLD;
}
Key getDimension();
/**
* Attempts to persist all changes that have been made in a world to disk.
*
* @return <code>true</code> if the changes have been successfully persisted, <code>false</code> if this operation is not supported by the implementation
*
* @throws IOException if something went wrong trying to persist the changes
*/
default boolean persistWorldChanges() throws IOException {

View File

@ -29,11 +29,15 @@ import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import de.bluecolored.bluemap.core.BlueMap;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
@Getter @Setter
public class BlueMapResponseModifier implements HttpRequestHandler {
private final HttpRequestHandler delegate;
private final String serverName;
private @NonNull HttpRequestHandler delegate;
private @NonNull String serverName;
public BlueMapResponseModifier(HttpRequestHandler delegate) {
this.delegate = delegate;

View File

@ -1,3 +1,27 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web;
import java.util.concurrent.locks.ReentrantLock;

View File

@ -25,37 +25,45 @@
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.web.http.*;
import org.apache.commons.lang3.time.DateFormatUtils;
import de.bluecolored.bluemap.core.logger.Logger;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.concurrent.TimeUnit;
@Getter @Setter
public class FileRequestHandler implements HttpRequestHandler {
private final Path webRoot;
private final File emptyTileFile;
private @NonNull Path webRoot;
public FileRequestHandler(Path webRoot) {
this.webRoot = webRoot.normalize();
this.emptyTileFile = webRoot.resolve("assets").resolve("emptyTile.json").toFile();
}
@Override
public HttpResponse handle(HttpRequest request) {
if (!request.getMethod().equalsIgnoreCase("GET"))
return new HttpResponse(HttpStatusCode.BAD_REQUEST);
return generateResponse(request);
try {
return generateResponse(request);
} catch (IOException e) {
Logger.global.logError("Failed to serve file", e);
return new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
}
private HttpResponse generateResponse(HttpRequest request) {
private HttpResponse generateResponse(HttpRequest request) throws IOException {
String path = request.getPath();
// normalize path
@ -74,53 +82,49 @@ public class FileRequestHandler implements HttpRequestHandler {
return new HttpResponse(HttpStatusCode.FORBIDDEN);
}
File file = filePath.toFile();
// redirect to have correct relative paths
if (file.isDirectory() && !request.getPath().endsWith("/")) {
if (Files.isDirectory(filePath) && !request.getPath().endsWith("/")) {
HttpResponse response = new HttpResponse(HttpStatusCode.SEE_OTHER);
response.addHeader("Location", "/" + path + "/" + (request.getGETParamString().isEmpty() ? "" : "?" + request.getGETParamString()));
return response;
}
// default to index.html
if (!file.exists() || file.isDirectory()){
file = new File(filePath + "/index.html");
if (!Files.exists(filePath) || Files.isDirectory(filePath)){
filePath = filePath.resolve("index.html");
}
// send empty tile-file if tile not exists
if (!file.exists() && file.toPath().startsWith(webRoot.resolve("maps"))){
file = emptyTileFile;
}
if (!file.exists() || file.isDirectory()) {
if (!Files.exists(filePath) || Files.isDirectory(filePath)){
return new HttpResponse(HttpStatusCode.NOT_FOUND);
}
// don't send php files
if (file.getName().endsWith(".php")) {
if (filePath.getFileName().toString().endsWith(".php")) {
return new HttpResponse(HttpStatusCode.FORBIDDEN);
}
// check if file is still in web-root and is not a directory
if (!file.toPath().normalize().startsWith(webRoot) || file.isDirectory()){
if (!filePath.normalize().startsWith(webRoot) || Files.isDirectory(filePath)){
return new HttpResponse(HttpStatusCode.FORBIDDEN);
}
// check modified
long lastModified = file.lastModified();
long lastModified = Files.getLastModifiedTime(filePath).to(TimeUnit.MILLISECONDS);
HttpHeader modHeader = request.getHeader("If-Modified-Since");
if (modHeader != null){
try {
long since = stringToTimestamp(modHeader.getValue());
long since = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(modHeader.getValue())).toEpochMilli();
if (since + 1000 >= lastModified){
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
} catch (IllegalArgumentException ignored){}
} catch (DateTimeParseException ignored){}
}
//check ETag
String eTag = Long.toHexString(file.length()) + Integer.toHexString(file.hashCode()) + Long.toHexString(lastModified);
String eTag =
Long.toHexString(Files.size(filePath)) +
Integer.toHexString(filePath.hashCode()) +
Long.toHexString(lastModified);
HttpHeader etagHeader = request.getHeader("If-None-Match");
if (etagHeader != null){
if(etagHeader.getValue().equals(eTag)) {
@ -131,12 +135,15 @@ public class FileRequestHandler implements HttpRequestHandler {
//create response
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("ETag", eTag);
if (lastModified > 0) response.addHeader("Last-Modified", timestampToString(lastModified));
if (lastModified > 0) response.addHeader("Last-Modified", DateTimeFormatter.RFC_1123_DATE_TIME.format(Instant
.ofEpochMilli(lastModified)
.atOffset(ZoneOffset.UTC)
));
response.addHeader("Cache-Control", "public");
response.addHeader("Cache-Control", "max-age=" + TimeUnit.HOURS.toSeconds(1));
response.addHeader("Cache-Control", "max-age=" + TimeUnit.DAYS.toSeconds(1));
//add content type header
String filetype = file.getName();
String filetype = filePath.getFileName().toString();
int pointIndex = filetype.lastIndexOf('.');
if (pointIndex >= 0) filetype = filetype.substring(pointIndex + 1);
String contentType = toContentType(filetype);
@ -144,80 +151,29 @@ public class FileRequestHandler implements HttpRequestHandler {
//send response
try {
response.setData(new FileInputStream(file));
response.setData(Files.newInputStream(filePath));
return response;
} catch (FileNotFoundException e) {
return new HttpResponse(HttpStatusCode.NOT_FOUND);
}
}
private static String timestampToString(long time){
return DateFormatUtils.format(time, "EEE, dd MMM yyy HH:mm:ss 'GMT'", TimeZone.getTimeZone("GMT"), Locale.ENGLISH);
}
private static long stringToTimestamp(String timeString) throws IllegalArgumentException {
try {
int day = Integer.parseInt(timeString.substring(5, 7));
int month = Calendar.JANUARY;
switch (timeString.substring(8, 11)){
case "Feb" : month = Calendar.FEBRUARY; break;
case "Mar" : month = Calendar.MARCH; break;
case "Apr" : month = Calendar.APRIL; break;
case "May" : month = Calendar.MAY; break;
case "Jun" : month = Calendar.JUNE; break;
case "Jul" : month = Calendar.JULY; break;
case "Aug" : month = Calendar.AUGUST; break;
case "Sep" : month = Calendar.SEPTEMBER; break;
case "Oct" : month = Calendar.OCTOBER; break;
case "Nov" : month = Calendar.NOVEMBER; break;
case "Dec" : month = Calendar.DECEMBER; break;
}
int year = Integer.parseInt(timeString.substring(12, 16));
int hour = Integer.parseInt(timeString.substring(17, 19));
int min = Integer.parseInt(timeString.substring(20, 22));
int sec = Integer.parseInt(timeString.substring(23, 25));
GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
cal.set(year, month, day, hour, min, sec);
return cal.getTimeInMillis();
} catch (NumberFormatException | IndexOutOfBoundsException e){
throw new IllegalArgumentException(e);
}
}
private static String toContentType(String fileEnding) {
String contentType = "text/plain";
switch (fileEnding) {
case "json" :
contentType = "application/json";
break;
case "png" :
contentType = "image/png";
break;
case "jpg" :
case "jpeg" :
case "jpe" :
contentType = "image/jpeg";
break;
case "svg" :
contentType = "image/svg+xml";
break;
case "css" :
contentType = "text/css";
break;
case "js" :
contentType = "text/javascript";
break;
case "html" :
case "htm" :
case "shtml" :
contentType = "text/html";
break;
case "xml" :
contentType = "text/xml";
break;
}
return contentType;
return switch (fileEnding) {
case "json" -> "application/json";
case "png" -> "image/png";
case "jpg",
"jpeg",
"jpe" -> "image/jpeg";
case "svg" -> "image/svg+xml";
case "css" -> "text/css";
case "js" -> "text/javascript";
case "html",
"htm",
"shtml" -> "text/html";
case "xml" -> "text/xml";
default -> "text/plain";
};
}
}

View File

@ -28,12 +28,16 @@ import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import java.util.function.Supplier;
@Getter @Setter
public class JsonDataRequestHandler implements HttpRequestHandler {
private final Supplier<String> dataSupplier;
private @NonNull Supplier<String> dataSupplier;
public JsonDataRequestHandler(Supplier<String> dataSupplier) {
this.dataSupplier = dataSupplier;

View File

@ -1,41 +1,98 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.*;
import de.bluecolored.bluemap.core.logger.Logger;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
@Getter @Setter
@AllArgsConstructor
public class LoggingRequestHandler implements HttpRequestHandler {
private final HttpRequestHandler delegate;
private final Logger logger;
private @NonNull HttpRequestHandler delegate;
private @NonNull String format;
private @NonNull Logger logger;
public LoggingRequestHandler(HttpRequestHandler delegate) {
this(delegate, Logger.global);
}
public LoggingRequestHandler(HttpRequestHandler delegate, Logger logger) {
this.delegate = delegate;
this.logger = logger;
this(delegate, "", logger);
}
public LoggingRequestHandler(HttpRequestHandler delegate, String format) {
this(delegate, format, Logger.global);
}
@Override
public HttpResponse handle(HttpRequest request) {
String log = request.getSource() + " \""
+ request.getMethod()
+ " " + request.getAddress()
+ " " + request.getVersion()
+ "\" ";
// gather format parameters from request
String source = request.getSource().toString();
String xffSource = source;
HttpHeader xffHeader = request.getHeader("X-Forwarded-For");
if (xffHeader != null && !xffHeader.getValues().isEmpty()) {
xffSource = xffHeader.getValues().get(0);
}
String method = request.getMethod();
String address = request.getAddress();
String version = request.getVersion();
// run request
HttpResponse response = delegate.handle(request);
log += response.getStatusCode().toString();
if (response.getStatusCode().getCode() < 400) {
// gather format parameters from response
HttpStatusCode status = response.getStatusCode();
int statusCode = status.getCode();
String statusMessage = status.getMessage();
// format log message
String log = String.format(this.format,
source,
xffSource,
method,
address,
version,
statusCode,
statusMessage
);
// do the logging
if (statusCode < 500) {
logger.logInfo(log);
} else {
logger.logWarning(log);
}
// return the response
return response;
}

View File

@ -1,10 +1,36 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.config.PluginConfig;
import de.bluecolored.bluemap.common.live.LiveMarkersDataSupplier;
import de.bluecolored.bluemap.common.live.LivePlayersDataSupplier;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import org.jetbrains.annotations.Nullable;
@ -14,21 +40,21 @@ import java.util.function.Supplier;
public class MapRequestHandler extends RoutingRequestHandler {
public MapRequestHandler(BmMap map, ServerInterface serverInterface, PluginConfig pluginConfig, Predicate<UUID> playerFilter) {
this(map.getId(), map.getStorage(),
new LivePlayersDataSupplier(serverInterface, pluginConfig, map.getWorldId(), playerFilter),
public MapRequestHandler(BmMap map, Server serverInterface, PluginConfig pluginConfig, Predicate<UUID> playerFilter) {
this(map.getStorage(),
createPlayersDataSupplier(map, serverInterface, pluginConfig, playerFilter),
new LiveMarkersDataSupplier(map.getMarkerSets()));
}
public MapRequestHandler(String mapId, Storage mapStorage) {
this(mapId, mapStorage, null, null);
public MapRequestHandler(MapStorage mapStorage) {
this(mapStorage, null, null);
}
public MapRequestHandler(String mapId, Storage mapStorage,
public MapRequestHandler(MapStorage mapStorage,
@Nullable Supplier<String> livePlayersDataSupplier,
@Nullable Supplier<String> liveMarkerDataSupplier) {
register(".*", new MapStorageRequestHandler(mapId, mapStorage));
register(".*", new MapStorageRequestHandler(mapStorage));
if (livePlayersDataSupplier != null) {
register("live/players\\.json", "", new JsonDataRequestHandler(
@ -43,4 +69,10 @@ public class MapRequestHandler extends RoutingRequestHandler {
}
}
private static @Nullable LivePlayersDataSupplier createPlayersDataSupplier(BmMap map, Server serverInterface, PluginConfig pluginConfig, Predicate<UUID> playerFilter) {
ServerWorld world = serverInterface.getServerWorld(map.getWorld()).orElse(null);
if (world == null) return null;
return new LivePlayersDataSupplier(serverInterface, pluginConfig, world, playerFilter);
}
}

View File

@ -24,41 +24,39 @@
*/
package de.bluecolored.bluemap.common.web;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.ContentTypeRegistry;
import de.bluecolored.bluemap.common.web.http.*;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.TileInfo;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.io.*;
import java.util.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RequiredArgsConstructor
@Getter @Setter
public class MapStorageRequestHandler implements HttpRequestHandler {
private static final Pattern TILE_PATTERN = Pattern.compile("tiles/([\\d/]+)/x(-?[\\d/]+)z(-?[\\d/]+).*");
private final String mapId;
private final Storage mapStorage;
public MapStorageRequestHandler(BmMap map) {
this.mapId = map.getId();
this.mapStorage = map.getStorage();
}
public MapStorageRequestHandler(String mapId, Storage mapStorage) {
this.mapId = mapId;
this.mapStorage = mapStorage;
}
private @NonNull MapStorage mapStorage;
@SuppressWarnings("resource")
@Override
public HttpResponse handle(HttpRequest request) {
String path = request.getPath();
@ -75,53 +73,36 @@ public class MapStorageRequestHandler implements HttpRequestHandler {
int lod = Integer.parseInt(tileMatcher.group(1));
int x = Integer.parseInt(tileMatcher.group(2).replace("/", ""));
int z = Integer.parseInt(tileMatcher.group(3).replace("/", ""));
Optional<TileInfo> optTileInfo = mapStorage.readMapTileInfo(mapId, lod, new Vector2i(x, z));
if (optTileInfo.isPresent()) {
TileInfo tileInfo = optTileInfo.get();
GridStorage gridStorage = lod == 0 ? mapStorage.hiresTiles() : mapStorage.lowresTiles(lod);
CompressedInputStream in = gridStorage.read(x, z);
if (in == null) return new HttpResponse(HttpStatusCode.NO_CONTENT);
// check e-tag
String eTag = calculateETag(path, tileInfo);
HttpHeader etagHeader = request.getHeader("If-None-Match");
if (etagHeader != null){
if(etagHeader.getValue().equals(eTag)) {
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
}
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("Cache-Control", "public");
response.addHeader("Cache-Control", "max-age=" + TimeUnit.DAYS.toSeconds(1));
// check modified-since
long lastModified = tileInfo.getLastModified();
HttpHeader modHeader = request.getHeader("If-Modified-Since");
if (modHeader != null){
try {
long since = stringToTimestamp(modHeader.getValue());
if (since + 1000 >= lastModified){
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
} catch (IllegalArgumentException ignored){}
}
if (lod == 0) response.addHeader("Content-Type", "application/octet-stream");
else response.addHeader("Content-Type", "image/png");
CompressedInputStream compressedIn = tileInfo.readMapTile();
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("ETag", eTag);
if (lastModified > 0)
response.addHeader("Last-Modified", timestampToString(lastModified));
if (lod == 0) response.addHeader("Content-Type", "application/json");
else response.addHeader("Content-Type", "image/png");
writeToResponse(compressedIn, response, request);
return response;
}
writeToResponse(in, response, request);
return response;
}
// provide meta-data
Optional<InputStream> optIn = mapStorage.readMeta(mapId, path);
if (optIn.isPresent()) {
CompressedInputStream compressedIn = new CompressedInputStream(optIn.get(), Compression.NONE);
CompressedInputStream in = switch (path) {
case "settings.json" -> mapStorage.settings().read();
case "textures.json" -> mapStorage.textures().read();
case "live/markers.json" -> mapStorage.markers().read();
case "live/players.json" -> mapStorage.players().read();
default -> path.startsWith("assets/") ? mapStorage.asset(path.substring(7)).read() : null;
};
if (in != null){
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("Cache-Control", "public");
response.addHeader("Cache-Control", "max-age=" + TimeUnit.DAYS.toSeconds(1));
response.addHeader("Content-Type", ContentTypeRegistry.fromFileName(path));
writeToResponse(compressedIn, response, request);
writeToResponse(in, response, request);
return response;
}
@ -131,41 +112,26 @@ public class MapStorageRequestHandler implements HttpRequestHandler {
return new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
if (path.endsWith(".png")) {
return new HttpResponse(HttpStatusCode.NO_CONTENT);
}
if (path.endsWith(".json")) {
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("Content-Type", "application/json");
response.setData("{}");
return response;
}
return new HttpResponse(HttpStatusCode.NOT_FOUND);
}
private String calculateETag(String path, TileInfo tileInfo) {
return Long.toHexString(tileInfo.getSize()) + Integer.toHexString(path.hashCode()) + Long.toHexString(tileInfo.getLastModified());
}
private void writeToResponse(CompressedInputStream data, HttpResponse response, HttpRequest request) throws IOException {
Compression compression = data.getCompression();
if (
compression != Compression.NONE &&
request.hasHeaderValue("Accept-Encoding", compression.getTypeId())
request.hasHeaderValue("Accept-Encoding", compression.getId())
) {
response.addHeader("Content-Encoding", compression.getTypeId());
response.addHeader("Content-Encoding", compression.getId());
response.setData(data);
} else if (
compression != Compression.GZIP &&
!response.hasHeaderValue("Content-Type", "image/png") &&
request.hasHeaderValue("Accept-Encoding", Compression.GZIP.getTypeId())
request.hasHeaderValue("Accept-Encoding", Compression.GZIP.getId())
) {
response.addHeader("Content-Encoding", Compression.GZIP.getTypeId());
response.addHeader("Content-Encoding", Compression.GZIP.getId());
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
try (OutputStream os = Compression.GZIP.compress(byteOut)) {
IOUtils.copyLarge(data.decompress(), os);
data.decompress().transferTo(os);
}
byte[] compressedData = byteOut.toByteArray();
response.setData(new ByteArrayInputStream(compressedData));
@ -174,38 +140,4 @@ public class MapStorageRequestHandler implements HttpRequestHandler {
}
}
private static String timestampToString(long time){
return DateFormatUtils.format(time, "EEE, dd MMM yyy HH:mm:ss 'GMT'", TimeZone.getTimeZone("GMT"), Locale.ENGLISH);
}
private static long stringToTimestamp(String timeString) throws IllegalArgumentException {
try {
int day = Integer.parseInt(timeString.substring(5, 7));
int month = Calendar.JANUARY;
switch (timeString.substring(8, 11)){
case "Feb" : month = Calendar.FEBRUARY; break;
case "Mar" : month = Calendar.MARCH; break;
case "Apr" : month = Calendar.APRIL; break;
case "May" : month = Calendar.MAY; break;
case "Jun" : month = Calendar.JUNE; break;
case "Jul" : month = Calendar.JULY; break;
case "Aug" : month = Calendar.AUGUST; break;
case "Sep" : month = Calendar.SEPTEMBER; break;
case "Oct" : month = Calendar.OCTOBER; break;
case "Nov" : month = Calendar.NOVEMBER; break;
case "Dec" : month = Calendar.DECEMBER; break;
}
int year = Integer.parseInt(timeString.substring(12, 16));
int hour = Integer.parseInt(timeString.substring(17, 19));
int min = Integer.parseInt(timeString.substring(20, 22));
int sec = Integer.parseInt(timeString.substring(23, 25));
GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
cal.set(year, month, day, hour, min, sec);
return cal.getTimeInMillis();
} catch (NumberFormatException | IndexOutOfBoundsException e){
throw new IllegalArgumentException(e);
}
}
}

View File

@ -28,18 +28,24 @@ import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import org.intellij.lang.annotations.Language;
import java.util.LinkedList;
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Getter
public class RoutingRequestHandler implements HttpRequestHandler {
public LinkedList<Route> routes;
private final Deque<Route> routes;
public RoutingRequestHandler() {
this.routes = new LinkedList<>();
this.routes = new ConcurrentLinkedDeque<>();
}
public void register(@Language("RegExp") String pattern, HttpRequestHandler handler) {
@ -77,36 +83,20 @@ public class RoutingRequestHandler implements HttpRequestHandler {
return new HttpResponse(HttpStatusCode.BAD_REQUEST);
}
private static class Route {
@AllArgsConstructor
@Getter @Setter
public static class Route {
private final Pattern routePattern;
private final HttpRequestHandler handler;
private final String replacementRoute;
private @NonNull Pattern routePattern;
private @NonNull String replacementRoute;
private @NonNull HttpRequestHandler handler;
public Route(Pattern routePattern, HttpRequestHandler handler) {
public Route(@NonNull Pattern routePattern, @NonNull HttpRequestHandler handler) {
this.routePattern = routePattern;
this.replacementRoute = "$0";
this.handler = handler;
}
public Route(Pattern routePattern, String replacementRoute, HttpRequestHandler handler) {
this.routePattern = routePattern;
this.replacementRoute = replacementRoute;
this.handler = handler;
}
public Pattern getRoutePattern() {
return routePattern;
}
public HttpRequestHandler getHandler() {
return handler;
}
public String getReplacementRoute() {
return replacementRoute;
}
}
}

View File

@ -1,3 +1,27 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web.http;
import de.bluecolored.bluemap.core.logger.Logger;
@ -63,13 +87,20 @@ public class HttpConnection implements SelectionConsumer {
() -> requestHandler.handle(request),
responseHandlerExecutor
);
futureResponse.thenAccept(response -> {
futureResponse.handle((response, error) -> {
if (error != null) {
Logger.global.logError("Unexpected error handling request", error);
response = new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
try {
response.read(channel); // do an initial read to trigger response sending intent
this.response = response;
} catch (IOException e) {
handleIOException(channel, e);
}
return null;
});
}
@ -96,7 +127,26 @@ public class HttpConnection implements SelectionConsumer {
private void handleIOException(Channel channel, IOException e) {
request.clear();
response = null;
if (response != null) {
try {
response.close();
} catch (IOException e2) {
Logger.global.logWarning("Failed to close response: " + e2);
}
response = null;
}
if (futureResponse != null) {
futureResponse.thenAccept(response -> {
try {
response.close();
} catch (IOException e2) {
Logger.global.logWarning("Failed to close response: " + e2);
}
});
futureResponse = null;
}
Logger.global.logDebug("Failed to process selection: " + e);
try {

View File

@ -1,3 +1,27 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web.http;
import java.util.*;

View File

@ -1,3 +1,27 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web.http;
import java.io.ByteArrayInputStream;

View File

@ -1,10 +1,38 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web.http;
import lombok.Getter;
import lombok.Setter;
import java.io.IOException;
public class HttpServer extends Server {
private final HttpRequestHandler requestHandler;
@Getter @Setter
private HttpRequestHandler requestHandler;
public HttpServer(HttpRequestHandler requestHandler) throws IOException {
this.requestHandler = requestHandler;
@ -13,10 +41,6 @@ public class HttpServer extends Server {
@Override
public SelectionConsumer createConnectionHandler() {
return new HttpConnection(requestHandler);
// Enable async request handling ...
// TODO: maybe find a better/separate executor than using bluemap's common thread-pool
//return new HttpConnection(requestHandler, BlueMap.THREAD_POOL);
}
}

View File

@ -24,6 +24,9 @@
*/
package de.bluecolored.bluemap.common.web.http;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum HttpStatusCode {
CONTINUE (100, "Continue"),
@ -47,13 +50,8 @@ public enum HttpStatusCode {
SERVICE_UNAVAILABLE (503, "Service Unavailable"),
HTTP_VERSION_NOT_SUPPORTED (505, "HTTP Version not supported");
private int code;
private String message;
private HttpStatusCode(int code, String message) {
this.code = code;
this.message = message;
}
private final int code;
private final String message;
public int getCode(){
return code;

View File

@ -1,3 +1,27 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web.http;
import java.nio.channels.SelectionKey;

View File

@ -1,13 +1,39 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web.http;
import de.bluecolored.bluemap.core.logger.Logger;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
public abstract class Server extends Thread implements Closeable, Runnable {
@ -28,7 +54,20 @@ public abstract class Server extends Thread implements Closeable, Runnable {
server.bind(address);
this.server.add(server);
Logger.global.logInfo("WebServer bound to: " + server.getLocalAddress());
if (checkIfBoundToAllInterfaces(address)) {
Logger.global.logInfo("WebServer bound to all network interfaces on port " + ((InetSocketAddress) address).getPort());
} else {
Logger.global.logInfo("WebServer bound to: " + server.getLocalAddress());
}
}
private boolean checkIfBoundToAllInterfaces(SocketAddress address) {
if (address instanceof InetSocketAddress) {
InetSocketAddress inetAddress = (InetSocketAddress) address;
return Objects.equals(inetAddress.getAddress(), new InetSocketAddress(0).getAddress());
}
return false;
}
@Override

View File

@ -5,7 +5,7 @@
# By changing the setting (accept-download) below to TRUE you are indicating that you have accepted mojang's EULA (https://account.mojang.com/documents/minecraft_eula),
# you confirm that you own a license to Minecraft (Java Edition)
# and you agree that BlueMap will download and use a minecraft-client file (depending on the minecraft-version) from mojangs servers (https://launcher.mojang.com/) for you.
# and you agree that BlueMap will download and use a minecraft-client file (depending on the minecraft-version) from mojangs servers (https://piston-meta.mojang.com/) for you.
# This file contains resources that belong to mojang and you must not redistribute it or do anything else that is not compliant with mojang's EULA.
# BlueMap uses resources in this file to generate the 3D-Models used for the map and texture them. (BlueMap will not work without those resources.)
# ${timestamp}
@ -32,4 +32,17 @@ ${metrics<<
# An example report looks like this: {"implementation":"${implementation}","version":"${version}"}
# Default is true
metrics: true
>>}
>>}
# Config-section for debug-logging
log: {
# The file where the debug-log will be written to.
# Comment out to disable debug-logging completely.
# Java String formatting syntax can be used to add time, see: https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html
# Default is no logging
file: "${logfile}"
#file: "${logfile-with-time}"
# Whether the logger should append to an existing file, or overwrite it
# Default is false
append: false
}

View File

@ -3,16 +3,20 @@
## Map-Config ##
## ##
# The name of this map
# This defines the display name of this map, you can change this at any time.
# Default is the id of this map
name: "${name}"
# The path to the save-folder of the world to render.
# (If this is not defined (commented out or removed), the map will be only registered to the web-server and the web-app
# but not rendered or loaded by BlueMap. This can be used to display a map that has been rendered somewhere else.)
world: "${world}"
# The dimension of the world. Can be "minecraft:overworld", "minecraft:the_nether", "minecraft:the_end"
# or any dimension-key introduced by a mod or datapack.
dimension: "${dimension}"
# The display-name of this map -> how this map will be named on the webapp.
# You can change this at any time.
# Default is the id of this map
name: "${name}"
# A lower value makes the map sorted first (in lists and menus), a higher value makes it sorted later.
# The value needs to be an integer but it can be negative.
# You can change this at any time.
@ -24,25 +28,22 @@ sorting: ${sorting}
# This defaults to the world-spawn if you don't set it.
#start-pos: {x:500, z:-820}
# The color of thy sky as a hex-color
# The color of the sky as a hex-color
# You can change this at any time.
# Default is "#7dabff"
sky-color: "${sky-color}"
# The color of the void as a hex-color
# You can change this at any time.
# Default is "#000000"
void-color: "${void-color}"
# Defines the ambient light-strength that every block is receiving, regardless of the sunlight/blocklight.
# 0 is no ambient light, 1 is fully lighted.
# You can change this at any time.
# Default is 0
ambient-light: ${ambient-light}
# Defines the skylight level that the sky of the world is emitting.
# This should always be equivalent to the maximum in-game sky-light for that world!
# If this is a normal overworld dimension, set this to 15 (max).
# If this is a normal nether or end dimension, set this to 0 (min).
# Changing this value requires a re-render of the map.
# Default is 15
world-sky-light: ${world-sky-light}
# BlueMap tries to omit all blocks that are below this Y-level and are not visible from above-ground.
# More specific: Block-Faces that have a sunlight/skylight value of 0 are removed.
# This improves the performance of the map on slower devices by a lot, but might cause some blocks to disappear that should normally be visible.
@ -115,7 +116,7 @@ ignore-missing-light-data: false
# Here you can define any static marker-sets with markers that should be displayed on the map.
# You can change this at any time.
# If you need dynamic markers, you can use any plugin that integrates with BlueMap's API.
# Here is a list: https://bluemap.bluecolored.de/wiki/customization/3rdPartySupport.html
# Here is a list: https://bluemap.bluecolored.de/community/3rdPartySupport.html
marker-sets: {
# Please check out the wiki for information on how to configure this:

View File

@ -65,8 +65,8 @@ skin-download: true
# Default is -1
player-render-limit: -1
# The interval in minutes in which a map-update will be triggered.
# This is additionally to the normal map-update process (in case that fails to detect any file-changes).
# The interval in minutes in which a full map-update will be triggered.
# This is ADDITIONALLY to the normal map-update process (in case that fails to detect any file-changes).
# ! This DOESN'T re-render the entire map each time, it only checks if there are some changes that have not been rendered yet!
# Default is 1440 (24 hours)
map-update-interval: 1440
full-update-interval: 1440

View File

@ -6,7 +6,7 @@
# The storage-type of this storage.
# Depending on this setting, different config-entries are allowed/expected in this config file.
# Don't change this value! (If you want a different storage-type, check out the other example-configs)
storage-type: FILE
storage-type: file
# The path to the folder on your file-system where bluemap will save the rendered map
# The default is: "bluemap/web/maps"
@ -14,7 +14,9 @@ root: "${root}"
# The compression-type that bluemap will use to compress generated map-data.
# Available compression-types are:
# - GZIP
# - NONE
# The default is: GZIP
compression: GZIP
# - gzip
# - zstd
# - deflate
# - none
# The default is: gzip
compression: gzip

View File

@ -6,7 +6,7 @@
# The storage-type of this storage.
# Depending on this setting, different config-entries are allowed/expected in this config file.
# Don't change this value! (If you want a different storage-type, check out the other example-configs)
storage-type: SQL
storage-type: sql
# The JDBC-Connection URL that is used to connect to the database.
# The format for this url is usually something like: jdbc:[driver]://[host]:[port]/[database]
@ -39,7 +39,9 @@ max-connections: -1
# The compression-type that bluemap will use to compress generated map-data.
# Available compression-types are:
# - GZIP
# - NONE
# The default is: GZIP
compression: GZIP
# - gzip
# - zstd
# - deflate
# - none
# The default is: gzip
compression: gzip

View File

@ -26,6 +26,10 @@ use-cookies: true
# Default is true
enable-free-flight: true
# If the webapp will default to flat-view instead of perspective-view.
# Default is false
default-to-flat-view: false
# The default map and camera-location where a user will start after opening the webapp.
# This is in form of the url-anchor: Open your map in a browser and look at the url, everything after the '#' is the value for this setting.
# Default is "no anchor" -> The camera will start with the topmost map and at that map's starting point.

View File

@ -16,3 +16,30 @@ webroot: "${webroot}"
# The port that the webserver listens to.
# Default is 8100
port: 8100
# Config-section for webserver-activity logging
log: {
# The file where all the webserver-activity will be logged to.
# Comment out to disable the logging completely.
# Java String formatting syntax can be used to add time, see: https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html
# Default is no logging
file: "${logfile}"
#file: "${logfile-with-time}"
# Whether the logger should append to an existing file, or overwrite it
# Default is false
append: false
# The format of the webserver-acivity logs.
# The syntax is the java String formatting, see: https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html
# Possible Arguments:
# 1 - the source address (ignoring any xff headers)
# 2 - the source address (using the (leftmost) xff header if provided)
# 3 - the http-method of the request
# 4 - the full request-address
# 5 - the protocol version of the request
# 6 - the status-code of the response
# 7 - the status-message of the response
# Default is "%1$s \"%3$s %4$s %5$s\" %6$s %7$s"
format: "%1$s \"%3$s %4$s %5$s\" %6$s %7$s"
}

File diff suppressed because it is too large Load Diff

View File

@ -16,10 +16,10 @@
"hocon-parser": "^1.0.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue": "^4.5.3",
"eslint": "~8.22.0",
"eslint-plugin-vue": "^9.3.0",
"sass": "^1.57.0",
"vite": "^4.0.0"
"vite": "^4.5.3"
}
}

View File

@ -1,192 +0,0 @@
<?php
// !!! SET YOUR SQL-CONNECTION SETTINGS HERE: !!!
$hostname = '127.0.0.1';
$port = 3306;
$username = 'root';
$password = '';
$database = 'bluemap';
// set this to "none" if you disabled compression on your maps
$hiresCompression = 'gzip';
// !!! END - DONT CHANGE ANYTHING AFTER THIS LINE !!!
// some helper functions
function error($code, $message = null) {
global $path;
http_response_code($code);
header("Content-Type: text/plain");
echo "BlueMap php-script - $code\n";
if ($message != null) echo $message."\n";
echo "Requested Path: $path";
exit;
}
function startsWith($haystack, $needle) {
return substr($haystack, 0, strlen($needle)) === $needle;
}
// mime-types for meta-files
$mimeDefault = "application/octet-stream";
$mimeTypes = [
"txt" => "text/plain",
"css" => "text/css",
"csv" => "text/csv",
"htm" => "text/html",
"html" => "text/html",
"js" => "text/javascript",
"xml" => "text/xml",
"png" => "image/png",
"jpg" => "image/jpeg",
"jpeg" => "image/jpeg",
"gif" => "image/gif",
"webp" => "image/webp",
"tif" => "image/tiff",
"tiff" => "image/tiff",
"svg" => "image/svg+xml",
"json" => "application/json",
"mp3" => "audio/mpeg",
"oga" => "audio/ogg",
"wav" => "audio/wav",
"weba" => "audio/webm",
"mp4" => "video/mp4",
"mpeg" => "video/mpeg",
"webm" => "video/webm",
"ttf" => "font/ttf",
"woff" => "font/woff",
"woff2" => "font/woff2"
];
function getMimeType($path) {
global $mimeDefault, $mimeTypes;
$i = strrpos($path, ".");
if ($i === false) return $mimeDefault;
$s = strrpos($path, "/");
if ($s !== false && $i < $s) return $mimeDefault;
$suffix = substr($path, $i + 1);
if (isset($mimeTypes[$suffix]))
return $mimeTypes[$suffix];
return $mimeDefault;
}
// determine relative request-path
$root = dirname($_SERVER['PHP_SELF']);
if ($root === "/" || $root === "\\") $root = "";
$uriPath = $_SERVER['REQUEST_URI'];
$path = substr($uriPath, strlen($root));
// add /
if ($path === "") {
header("Location: $uriPath/");
exit;
}
// root => index.html
if ($path === "/") {
header("Content-Type: text/html");
echo file_get_contents("index.html");
exit;
}
if (startsWith($path, "/maps/")) {
// determine map-path
$pathParts = explode("/", substr($path, strlen("/maps/")), 2);
$mapId = $pathParts[0];
$mapPath = explode("?", $pathParts[1], 2)[0];
// get sql-connection
$sql = new mysqli($hostname, $username, $password, $database, $port);
if ($sql->errno) error(500, "Failed to connect to Database!");
// provide map-tiles
if (startsWith($mapPath, "tiles/")) {
// parse tile-coordinates
preg_match_all("/tiles\/([\d\/]+)\/x(-?[\d\/]+)z(-?[\d\/]+).*/", $mapPath, $matches);
$lod = intval($matches[1][0]);
$tileX = intval(str_replace("/", "", $matches[2][0]));
$tileZ = intval(str_replace("/", "", $matches[3][0]));
$compression = $lod === 0 ? $hiresCompression : "none";
// query for tile
$statement = $sql->prepare("
SELECT t.`data`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
");
$statement->bind_param("siiis", $mapId, $lod, $tileX, $tileZ, $compression);
$statement->execute();
if ($statement->errno) error(500, "Database query failed!");
// return result
$result = $statement->get_result();
if ($result && $line = $result->fetch_assoc()) {
if ($compression !== "none")
header("Content-Encoding: $compression");
if ($lod === 0) {
header("Content-Type: application/json");
} else {
header("Content-Type: image/png");
}
echo $line["data"];
exit;
}
// empty json response if nothing found
header("Content-Type: application/json");
echo "{}";
exit;
}
// provide meta-files
$statement = $sql->prepare("
SELECT t.`value`
FROM `bluemap_map_meta` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
AND t.`key` = ?
");
$statement->bind_param("ss", $mapId, $mapPath);
$statement->execute();
if ($statement->errno) error(500, "Database query failed!");
$result = $statement->get_result();
if ($result && $line = $result->fetch_assoc()) {
header("Content-Type: ".getMimeType($mapPath));
echo $line["value"];
exit;
}
}
// no match => 404
error(404);

View File

@ -83,6 +83,9 @@
light: "Light"
contrast: "Contrast"
}
chunkBorders: {
button: "Show chunk borders"
}
debug: {
button: "Debug"
}

View File

@ -83,6 +83,9 @@
light: "Vaalea"
contrast: "Kontrasti"
}
chunkBorders: {
button: "Näytä lohkojen rajat"
}
debug: {
button: "Debug"
}

View File

@ -4,6 +4,11 @@
title: "Menu"
tooltip: "Menu"
}
map: {
unloaded: "Pas de carte chargée."
loading: "Chargement de la carte..."
errored: "Il y a eu une erreur lors du chargement de la carte !"
}
maps: {
title: "Cartes"
button: "Cartes"
@ -13,10 +18,18 @@
title: "Balises"
button: "Balises"
tooltip: "Liste des balises"
marker: "balises | balises"
marker: "balise | balises"
markerSet: "Collection de balises | Collections de balises"
searchPlaceholder: "Rechercher..."
followPlayerTitle: "Suivre ce Joueur"
sort {
title: "Trier par"
by {
default: "défaut"
label: "nom"
distance: "distance"
}
}
}
settings: {
title: "Paramètres"
@ -38,7 +51,7 @@
dayNightSwitch: {
tooltip: "Jour/Nuit"
}
sunlight: "Soleil"
sunlight: "Lumière du Soleil"
ambientLight: "Lumière Ambiante"
}
resolution: {
@ -47,24 +60,31 @@
normal: "Normale (Native x1)"
low: "Basse (Upscaling x0.5)"
}
mapControls: {
title: "Contrôles de la Carte"
showZoomButtons: "Montrer les Boutons de Zoom"
}
freeFlightControls: {
title: "Contrôles du Vol Libre"
mouseSensitivity: "Sensibilité de la Souris"
invertMouseY: "Inverser l\'Y de la Souris"
}
renderDistance: {
title: "Distance de rendu"
hiresLayer: "Couche haute définition"
lowersLayer: "Couche basse définition"
title: "Distance de Rendu"
hiresLayer: "Couche Haute Définition"
lowersLayer: "Couche Basse Définition"
loadHiresWhileMoving: "Charger la haute définition en bougeant"
off: "Désactivé"
}
theme: {
title: "Thème"
default: "Par Défaut (Système/Navigateur)"
dark: "Sombre"
light: "Clair"
contrast: "Contrasté"
}
debug: {
button: "Debug"
button: "Debogage"
}
resetAllSettings: {
button: "Réinitialiser tous les Paramètres"
@ -74,7 +94,12 @@
tooltip: "Liste des Joueurs"
}
compass: {
tooltip: "Boussole / Pointe le Nord"
tooltip: "Boussole / Pointer le Nord"
}
screenshot: {
title: "Capture d\'écran"
button: "Prendre une Capture d\'écran"
clipboard: "Copier la Capture"
}
controls: {
title: "Vue / Contrôles"
@ -97,7 +122,7 @@
blockTooltip: {
block: "Bloc"
position: "Position"
chunk: "Chunk"
chunk: "Tronçon"
region: {
region: "Région"
file: "Fichier"
@ -116,25 +141,25 @@
<p>
<h2>Contrôles de la Souris :</h2>
<table>
<tr><th>déplacement</th><td><kbd>clic-gauche</kbd> + mouvement</td></tr>
<tr><th>zoom</th><td><kbd>molette</kbd> (scroll)</td></tr>
<tr><th>rotation / inclinaison</th><td><kbd>clic-droit</kbd> + mouvement</td></tr>
<tr><th>déplacer</th><td><kbd>clic-gauche</kbd> + glisser</td></tr>
<tr><th>zoomer</th><td><kbd>molette</kbd> (défiler)</td></tr>
<tr><th>tourner / incliner</th><td><kbd>clic-droit</kbd> + glisser</td></tr>
</table>
</p>
<p>
<h2>Contrôles du Clavier :</h2>
<table>
<tr><th>déplacement</th><td><kbd>zqsd</kbd> / <kbd>flèches</kbd></td></tr>
<tr><th>zoom</th><td>Pavé Numérique : <kbd>+</kbd>/<kbd>-</kbd> ou <kbd>Inser</kbd>/<kbd>Début</kbd></td></tr>
<tr><th>rotation / inclinaison</th><td><kbd>Alt-Gauche</kbd> + <kbd>zqsd</kbd> / <kbd>flèches</kbd> ou <kbd>Suppr</kbd>/<kbd>Fin</kbd>/<kbd>Page Up</kbd>/<kbd>Page Down</kbd></td></tr>
<tr><th>déplacer</th><td><kbd>zqsd</kbd> / <kbd>flèches directionnelles</kbd></td></tr>
<tr><th>zoomer</th><td>Pavé Numérique : <kbd>+</kbd>/<kbd>-</kbd> ou <kbd>Inser</kbd>/<kbd>Début</kbd></td></tr>
<tr><th>tourner / incliner</th><td><kbd>Alt-Gauche</kbd> + <kbd>zqsd</kbd> / <kbd>flèches</kbd> ou <kbd>Suppr</kbd>/<kbd>Fin</kbd>/<kbd>Page Haut</kbd>/<kbd>Page Bas</kbd></td></tr>
</table>
</p>
<p>
<h2>Contrôles du Toucher :</h2>
<h2>Contrôles Tactiles :</h2>
<table>
<tr><th>déplacement</th><td>toucher + mouvement</td></tr>
<tr><th>zoom</th><td>toucher avec 2 doigts + pincement</td></tr>
<tr><th>rotation / inclinaison</th><td>toucher avec 2 doigts + rotation / haut/bas</td></tr>
<tr><th>déplacer</th><td>toucher + glisser</td></tr>
<tr><th>zoomer</th><td>toucher avec 2 doigts + pincement</td></tr>
<tr><th>tourner / incliner</th><td>toucher avec 2 doigts + tourner / haut/bas</td></tr>
</table>
</p>
<br><hr>

View File

@ -4,6 +4,11 @@
title: "Menu"
tooltip: "Menu"
}
map: {
unloaded: "Geen kaart geladen."
loading: "Kaart laden..."
errored: "Er is een probleem opgetreden tijdens het laden van deze kaart!"
}
maps: {
title: "Kaarten"
button: "Kaarten"
@ -14,9 +19,17 @@
button: "Markers"
tooltip: "Markerlijst"
marker: "marker | markers"
markerSet: "Markerset | Markersets"
markerSet: "markerset | markersets"
searchPlaceholder: "Zoek..."
followPlayerTitle: "Volg Speler"
sort {
title: "Sorteer op"
by {
default: "standaard"
label: "naam"
distance: "afstand"
}
}
}
settings: {
title: "Instellingen"
@ -47,6 +60,10 @@
normal: "Normaal (Standaard x1)"
low: "Laag (Opgeschaald x0.5)"
}
mapControls: {
title: "Kaartbesturing"
showZoomButtons: "Laat zoomknoppen zien"
}
freeFlightControls: {
title: "Vrije camera"
mouseSensitivity: "Muis gevoeligheid"
@ -56,12 +73,18 @@
title: "Renderafstand"
hiresLayer: "Hires-Laag"
lowersLayer: "Lowres-Laag"
loadHiresWhileMoving: "Laad hires tijdens het bewegen"
off: "Uit"
}
theme: {
title: "Kleurmodus"
default: "Standaard (Systeem/Browser)"
dark: "Donker"
light: "Licht"
contrast: "Contrast"
}
chunkBorders: {
button: "Laat chunk grenzen zien"
}
debug: {
button: "Debug"
@ -76,6 +99,11 @@
compass: {
tooltip: "Kompas / Naar het noorden richten"
}
screenshot: {
title: "Schermafdruk"
button: "Schermafdruk nemen"
clipboard: "Kopieer naar klembord"
}
controls: {
title: "Aanzicht / Besturing"
perspective: {

View File

@ -4,6 +4,11 @@
title: "Меню"
tooltip: "Меню"
}
map: {
unloaded: "Карта не загружена."
loading: "Карта загружается..."
errored: "При загрузке этой карты произошла ошибка!"
}
maps: {
title: "Карта"
button: "Карта"
@ -17,6 +22,14 @@
markerSet: "набор маркеров | наборы маркеров"
searchPlaceholder: "Поиск..."
followPlayerTitle: "Следовать за игроком"
sort {
title: "Сортировать по"
by {
default: "умолчанию"
label: "имени"
distance: "расстоянию"
}
}
}
settings: {
title: "Настройки"
@ -27,7 +40,7 @@
}
resetCamera: {
button: "Сброс настроек камеры"
tooltip: "Сбросить настройки и положение камеры"
tooltip: "Сброс настроек и положения камеры"
}
updateMap: {
button: "Обновить карту"
@ -54,7 +67,7 @@
freeFlightControls: {
title: "Управление свободным полётом"
mouseSensitivity: "Чувствительность мыши"
invertMouseY: "Инвертировать мышь по Y"
invertMouseY: "Инвертировать мышь по оси Y"
}
renderDistance: {
title: "Дальность прорисовки"
@ -70,6 +83,9 @@
light: "Светлая"
contrast: "Контрастная"
}
chunkBorders: {
button: "Показывать границы чанков"
}
debug: {
button: "Отладка"
}
@ -116,8 +132,8 @@
}
light: {
light: "Освещение"
sun: "Неба"
block: "Блоками"
sun: "Небо"
block: "Блоки"
}
}
info: {
@ -129,7 +145,7 @@
<h2>Управление мышью:</h2>
<table>
<tr><th>перемещение</th><td>зажать <kbd>ЛКМ</kbd></td></tr>
<tr><th>приближение</th><td>прокрутить <kbd>колесо</kbd></td></tr>
<tr><th>приближение</th><td>прокрутить <kbd>колёсико</kbd></td></tr>
<tr><th>поворот / наклон</th><td>зажать <kbd>ПКМ</kbd></td></tr>
</table>
</p>

Some files were not shown because too many files have changed in this diff Show More