Compare commits

...

119 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
1206 changed files with 19415 additions and 24377 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

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

@ -24,9 +24,9 @@ jobs:
with:
distribution: 'temurin'
java-version: |
11
16
17
21
cache: 'gradle'
- name: Build with Gradle
run: ./gradlew clean spotlessCheck test build

View File

@ -2,9 +2,12 @@ name: Publish
on:
workflow_dispatch:
push:
tags:
- "**"
jobs:
publish:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -16,12 +19,12 @@ jobs:
with:
distribution: 'temurin'
java-version: |
11
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

2
.gitignore vendored
View File

@ -18,7 +18,7 @@ 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 5c3624832f17334f7b82dd40d7dbb6ba041163c4
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-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
BlueMapCommon/gradlew vendored
View File

@ -1,234 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${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 "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# 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" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
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,39 +108,17 @@ 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({"FieldMayBeFinal", "FieldCanBeLocal", "unused", "MismatchedQueryAndUpdateOfCollection"})
@ -197,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,16 +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.file;
package de.bluecolored.bluemap.common.addons;
import de.bluecolored.bluemap.core.storage.Compression;
import lombok.Getter;
import java.nio.file.Path;
@Getter
public class AddonInfo {
public static final String ADDON_INFO_FILE = "bluemap.addon.json";
public interface FileStorageSettings {
Path getRoot();
Compression getCompression();
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,25 +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.*;
import java.util.concurrent.ExecutionException;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
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()
@ -58,47 +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() {
Map<String, BmMap> maps = plugin.getMaps();
if (maps == null) return Collections.emptyList();
return maps.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() {
Map<String, World> worlds = plugin.getWorlds();
if (worlds == null) return Collections.emptyList();
return worlds.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());
}
@ -108,42 +103,25 @@ public class BlueMapAPIImpl extends BlueMapAPI {
}
public Optional<BlueMapWorld> getWorldUncached(Object world) {
var worlds = plugin.getWorlds();
if (worlds == null) return Optional.empty();
if (world instanceof UUID) {
var coreWorld = worlds.get(world.toString());
if (coreWorld != null) world = coreWorld;
}
if (world instanceof String) {
var coreWorld = worlds.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 = worlds.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));
}
@ -153,8 +131,7 @@ public class BlueMapAPIImpl extends BlueMapAPI {
}
public Optional<BlueMapMap> getMapUncached(String id) {
var maps = plugin.getMaps();
if (maps == null) return Optional.empty();
var maps = blueMapService.getMaps();
var map = maps.get(id);
if (map == null) return Optional.empty();
@ -162,7 +139,7 @@ public class BlueMapAPIImpl extends BlueMapAPI {
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
@ -170,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());
}
}
@ -181,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

@ -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,28 +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.*;
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;
@ -54,85 +66,59 @@ 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(), coreConfig.getData());
this.pluginConfig = usePluginConf ? loadPluginConfig() : new PluginConfig();
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(),
@ -143,20 +129,34 @@ 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, Path dataRoot) 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")))
@ -168,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
@ -191,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
);
@ -213,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,
@ -280,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" +
@ -289,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) {
@ -302,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) {
@ -335,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);
}
@ -357,43 +371,43 @@ 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");

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 {
@ -75,7 +73,6 @@ public class CoreConfig {
return log;
}
@DebugDump
@ConfigSerializable
public static class LogConfig {

View File

@ -26,45 +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;
@ -80,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;
@ -88,61 +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 String getVoidColor() {
return voidColor;
}
@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;
@ -153,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 {

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,7 +32,6 @@ import java.net.UnknownHostException;
import java.nio.file.Path;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
public class WebserverConfig {
@ -75,7 +73,6 @@ public class WebserverConfig {
return log;
}
@DebugDump
@ConfigSerializable
public static class LogConfig {

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::create);
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,40 +22,27 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.threejs;
package de.bluecolored.bluemap.common.config.typeserializer;
public class MaterialGroup {
private int materialIndex;
private int start;
private int count;
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 MaterialGroup(int materialIndex, int start, int count) {
this.materialIndex = materialIndex;
this.start = start;
this.count = count;
import java.lang.reflect.Type;
public class KeyTypeSerializer implements TypeSerializer<Key> {
@Override
public Key deserialize(Type type, ConfigurationNode node) {
String formatted = node.getString();
return formatted != null ? new Key(node.getString()) : null;
}
public int getMaterialIndex() {
return materialIndex;
@Override
public void serialize(Type type, @Nullable Key obj, ConfigurationNode node) throws SerializationException {
if (obj != null) node.set(obj.getFormatted());
}
public int getStart() {
return start;
}
public int getCount() {
return count;
}
public void setMaterialIndex(int materialIndex) {
this.materialIndex = materialIndex;
}
public void setStart(int start) {
this.start = start;
}
public void setCount(int count) {
this.count = count;
}
}
}

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;
@ -58,6 +63,7 @@ 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;
@ -67,7 +73,7 @@ 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";
@ -75,33 +81,28 @@ public class Plugin implements ServerEventListener {
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 Logger webLogger;
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;
@ -120,12 +121,25 @@ 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) {
@ -149,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");
@ -167,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();
@ -191,11 +207,11 @@ 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)
@ -215,7 +231,7 @@ public class Plugin implements ServerEventListener {
try {
webServer = new HttpServer(new LoggingRequestHandler(
routingRequestHandler,
webRequestHandler,
webserverConfig.getLog().getFormat(),
webLogger
));
@ -231,9 +247,11 @@ public class Plugin implements ServerEventListener {
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);
}
}
@ -276,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();
@ -306,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();
}
};
@ -330,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
@ -350,10 +369,6 @@ 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
@ -378,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();
@ -398,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();
@ -413,8 +437,11 @@ public class Plugin implements ServerEventListener {
Thread.currentThread().interrupt();
}
}
renderManager = null;
//save
save();
// stop webserver
if (webServer != null && !keepWebserver) {
try {
webServer.close();
@ -424,7 +451,7 @@ public class Plugin implements ServerEventListener {
webServer = null;
}
if (webLogger != null) {
if (webLogger != null && !keepWebserver) {
try {
webLogger.close();
} catch (Exception ex) {
@ -447,9 +474,6 @@ public class Plugin implements ServerEventListener {
Logger.global.remove(DEBUG_FILE_LOG_NAME);
//clear resources
worlds = null;
maps = null;
pluginState = null;
//done
@ -479,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);
@ -491,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) {
@ -502,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);
}
}
}
@ -542,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;
}
@ -588,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 &&
@ -604,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,13 +30,13 @@ 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 {
@ -97,7 +97,13 @@ public class CommandHelper {
long etaMs = renderer.estimateCurrentRenderTaskTimeRemaining();
if (etaMs > 0) {
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0ETA: ", 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));
}
}
}
@ -106,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, "\u00A0\u00A0\u00A0(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")
@ -134,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) {

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;
@ -47,25 +48,29 @@ import de.bluecolored.bluemap.common.plugin.text.TextFormat;
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.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.world.Block;
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.*;
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;
@ -121,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)
@ -167,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")
@ -222,6 +243,7 @@ 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);
@ -281,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());
}
}
@ -292,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);
}
}
@ -327,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;
}
@ -416,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();
}
@ -436,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 {
@ -465,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());
@ -482,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 {
@ -499,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));
@ -562,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();
@ -583,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;
}
@ -624,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;
}
@ -649,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
@ -671,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 {
@ -682,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;
}
}
@ -699,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;
}
@ -714,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);
}
@ -733,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."));
@ -790,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;
}
@ -831,8 +949,8 @@ 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;
@ -842,7 +960,7 @@ public class Commands<S> {
List<Text> lines = new ArrayList<>();
lines.add(Text.of(TextColor.BLUE, "Maps loaded by BlueMap:"));
for (BmMap map : plugin.getMaps().values()) {
for (BmMap map : plugin.getBlueMap().getMaps().values()) {
boolean frozen = !plugin.getPluginState().getMapState(map).isUpdateEnabled();
lines.add(Text.of(TextColor.GRAY, " - ",
@ -850,9 +968,9 @@ public class Commands<S> {
TextColor.GRAY, " (" + map.getName() + ")"));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0World: ",
TextColor.DARK_GRAY, map.getWorld().getName()));
TextColor.DARK_GRAY, map.getWorld().getId()));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0Last Update: ",
TextColor.DARK_GRAY, helper.formatTime(map.getRenderState().getLatestRenderTime())));
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!"));
@ -868,9 +986,14 @@ public class Commands<S> {
CommandSource source = commandSourceInterface.apply(context.getSource());
source.sendMessage(Text.of(TextColor.BLUE, "Storages loaded by BlueMap:"));
for (var entry : plugin.getBlueMap().getConfigs().getStorageConfigs().entrySet()) {
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(entry.getValue().getStorageType().name()))
.setHoverText(Text.of(storageTypeKey))
.setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap storages " + entry.getKey())
);
}
@ -884,18 +1007,19 @@ public class Commands<S> {
Storage storage;
try {
storage = plugin.getBlueMap().getStorage(storageId);
} catch (ConfigurationException ex) {
source.sendMessage(Text.of(TextColor.RED, ex.getMessage()));
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.collectMapIds();
mapIds = storage.mapIds().toList();
} catch (IOException ex) {
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to access this storage. Please check the console for more details..."));
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;
}
@ -904,8 +1028,8 @@ public class Commands<S> {
source.sendMessage(Text.of(TextColor.GRAY, " <empty storage>"));
} else {
for (String mapId : mapIds) {
BmMap map = plugin.getMaps().get(mapId);
boolean isLoaded = map != null && map.getStorage().equals(storage);
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)"));
@ -923,15 +1047,16 @@ public class Commands<S> {
String storageId = context.getArgument("storage", String.class);
String mapId = context.getArgument("map", String.class);
Storage storage;
MapStorage storage;
try {
storage = plugin.getBlueMap().getStorage(storageId);
} catch (ConfigurationException ex) {
source.sendMessage(Text.of(TextColor.RED, ex.getMessage()));
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.getMaps().get(mapId);
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)

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

@ -38,7 +38,7 @@ public class StorageSuggestionProvider<S> extends AbstractSuggestionProvider<S>
@Override
public Collection<String> getPossibleValues() {
return plugin.getBlueMap().getConfigs().getStorageConfigs().keySet();
return plugin.getBlueMap().getConfig().getStorageConfigs().keySet();
}
}

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;
@ -41,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

@ -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;
@ -84,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;
}
@ -111,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;
@ -55,19 +55,15 @@ public class MapPurgeTask implements RenderTask {
// 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

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,19 +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;
@DebugDump private long lastTimeBusy;
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();
@ -106,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) + "%)");
}
}
}
}
}

View File

@ -24,21 +24,21 @@
*/
package de.bluecolored.bluemap.common.rendermanager;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.Storage;
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 Storage storage;
private final MapStorage storage;
private final String mapId;
private volatile double progress;
private volatile boolean hasMoreWork;
private volatile boolean cancelled;
public StorageDeleteTask(Storage storage, String mapId) {
public StorageDeleteTask(MapStorage storage, String mapId) {
this.storage = Objects.requireNonNull(storage);
this.mapId = Objects.requireNonNull(mapId);
this.progress = 0d;
@ -55,8 +55,8 @@ public class StorageDeleteTask implements RenderTask {
if (this.cancelled) return;
// purge the map
storage.purgeMap(mapId, progressInfo -> {
this.progress = progressInfo.getProgress();
storage.delete(progress -> {
this.progress = progress;
return !this.cancelled;
});
}

View File

@ -26,208 +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);
Region region = map.getWorld().getRegion(worldRegion.getX(), worldRegion.getY());
Collection<Vector2i> chunks = region.listChunks(changesSince);
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid chunkGrid = map.getWorld().getChunkGrid();
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));
}
}
// make sure chunk gets re-loaded from disk
map.getWorld().invalidateChunkCache(chunk.getX(), chunk.getY());
// 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();
}
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;
// 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();
Vector2i cellMax = tileGrid.getCellMax(t);
if (cellMax.getX() < map.getMapSettings().getMinPos().getX()) return false;
return cellMax.getY() >= map.getMapSettings().getMinPos().getZ();
};
int tileIndex = tileIndex(x, z);
tileActions[tileIndex] = tileState.findActionAndNextState(
force.test(tileState) || checkChunksHaveChanges(tile),
checkTileBounds(tile)
);
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 (tileActions[tileIndex].action() == RENDER)
tileRenderCount++;
if (tileActions[tileIndex].action() == DELETE)
tileDeleteCount++;
}
}
if (tileRenderCount >= tileMaxCount * 0.75)
map.getWorld().preloadRegionChunks(regionPos.getX(), regionPos.getY());
if (tileRenderCount + tileDeleteCount == 0)
completed = true;
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
@ -235,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

@ -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

@ -26,12 +26,18 @@ package de.bluecolored.bluemap.common.web;
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 final String format;
private @NonNull HttpRequestHandler delegate;
private @NonNull String format;
private @NonNull Logger logger;
public LoggingRequestHandler(HttpRequestHandler delegate) {
this(delegate, Logger.global);
@ -45,12 +51,6 @@ public class LoggingRequestHandler implements HttpRequestHandler {
this(delegate, format, Logger.global);
}
public LoggingRequestHandler(HttpRequestHandler delegate, String format, Logger logger) {
this.delegate = delegate;
this.format = format;
this.logger = logger;
}
@Override
public HttpResponse handle(HttpRequest request) {

View File

@ -27,8 +27,10 @@ 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;
@ -38,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(
@ -67,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

@ -87,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;
});
}

View File

@ -24,11 +24,15 @@
*/
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;
@ -37,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

@ -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}

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.
@ -40,14 +44,6 @@ void-color: "${void-color}"
# 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.

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

@ -15,11 +15,11 @@
"vue-i18n": "^9.2.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.5",
"@vitejs/plugin-vue": "^4.5.3",
"eslint": "~8.22.0",
"eslint-plugin-vue": "^9.3.0",
"sass": "^1.57.0",
"vite": "^4.0.5"
"vite": "^4.5.3"
}
},
"node_modules/@babel/parser": {
@ -34,9 +34,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
"integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
@ -50,9 +50,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
"integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
@ -66,9 +66,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
"integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
@ -82,9 +82,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
"integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
@ -98,9 +98,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
"integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
@ -114,9 +114,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
"integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
@ -130,9 +130,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
"integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
@ -146,9 +146,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
"integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
@ -162,9 +162,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
"integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
@ -178,9 +178,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
"integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
@ -194,9 +194,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
"integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
@ -210,9 +210,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
"integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
@ -226,9 +226,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
"integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
@ -242,9 +242,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
"integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
@ -258,9 +258,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
"integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
@ -274,9 +274,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
"integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
@ -290,9 +290,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
"integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
@ -306,9 +306,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
"integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
@ -322,9 +322,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
"integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
@ -338,9 +338,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
"integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
@ -354,9 +354,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
"integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
@ -370,9 +370,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
"integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
@ -531,15 +531,15 @@
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
"integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
"dev": true,
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"vite": "^4.0.0",
"vite": "^4.0.0 || ^5.0.0",
"vue": "^3.2.25"
}
},
@ -949,9 +949,9 @@
}
},
"node_modules/esbuild": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
"integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"bin": {
@ -961,28 +961,28 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.17.19",
"@esbuild/android-arm64": "0.17.19",
"@esbuild/android-x64": "0.17.19",
"@esbuild/darwin-arm64": "0.17.19",
"@esbuild/darwin-x64": "0.17.19",
"@esbuild/freebsd-arm64": "0.17.19",
"@esbuild/freebsd-x64": "0.17.19",
"@esbuild/linux-arm": "0.17.19",
"@esbuild/linux-arm64": "0.17.19",
"@esbuild/linux-ia32": "0.17.19",
"@esbuild/linux-loong64": "0.17.19",
"@esbuild/linux-mips64el": "0.17.19",
"@esbuild/linux-ppc64": "0.17.19",
"@esbuild/linux-riscv64": "0.17.19",
"@esbuild/linux-s390x": "0.17.19",
"@esbuild/linux-x64": "0.17.19",
"@esbuild/netbsd-x64": "0.17.19",
"@esbuild/openbsd-x64": "0.17.19",
"@esbuild/sunos-x64": "0.17.19",
"@esbuild/win32-arm64": "0.17.19",
"@esbuild/win32-ia32": "0.17.19",
"@esbuild/win32-x64": "0.17.19"
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/escape-string-regexp": {
@ -1651,9 +1651,9 @@
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [
{
"type": "github",
@ -1816,9 +1816,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"funding": [
{
"type": "opencollective",
@ -1834,7 +1834,7 @@
}
],
"dependencies": {
"nanoid": "^3.3.6",
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
@ -1952,9 +1952,9 @@
}
},
"node_modules/rollup": {
"version": "3.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.0.tgz",
"integrity": "sha512-OgraHOIg2YpHQTjl0/ymWfFNBEyPucB7lmhXrQUh38qNOegxLapSPFs9sNr0qKR75awW41D93XafoR2QfhBdUQ==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@ -2008,9 +2008,9 @@
}
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@ -2179,14 +2179,14 @@
"dev": true
},
"node_modules/vite": {
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
"integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"dependencies": {
"esbuild": "^0.17.5",
"postcss": "^8.4.23",
"rollup": "^3.21.0"
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
"rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"
@ -2194,12 +2194,16 @@
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"stylus": "*",
"sugarss": "*",
@ -2212,6 +2216,9 @@
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
@ -2295,9 +2302,9 @@
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@ -2344,156 +2351,156 @@
"integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA=="
},
"@esbuild/android-arm": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
"integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"dev": true,
"optional": true
},
"@esbuild/android-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
"integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"dev": true,
"optional": true
},
"@esbuild/android-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
"integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"dev": true,
"optional": true
},
"@esbuild/darwin-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
"integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"dev": true,
"optional": true
},
"@esbuild/darwin-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
"integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"dev": true,
"optional": true
},
"@esbuild/freebsd-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
"integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"dev": true,
"optional": true
},
"@esbuild/freebsd-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
"integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"dev": true,
"optional": true
},
"@esbuild/linux-arm": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
"integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"dev": true,
"optional": true
},
"@esbuild/linux-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
"integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"dev": true,
"optional": true
},
"@esbuild/linux-ia32": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
"integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"dev": true,
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
"integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"dev": true,
"optional": true
},
"@esbuild/linux-mips64el": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
"integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"dev": true,
"optional": true
},
"@esbuild/linux-ppc64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
"integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"dev": true,
"optional": true
},
"@esbuild/linux-riscv64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
"integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"dev": true,
"optional": true
},
"@esbuild/linux-s390x": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
"integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"dev": true,
"optional": true
},
"@esbuild/linux-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
"integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"dev": true,
"optional": true
},
"@esbuild/netbsd-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
"integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"dev": true,
"optional": true
},
"@esbuild/openbsd-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
"integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"dev": true,
"optional": true
},
"@esbuild/sunos-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
"integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"dev": true,
"optional": true
},
"@esbuild/win32-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
"integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"dev": true,
"optional": true
},
"@esbuild/win32-ia32": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
"integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"dev": true,
"optional": true
},
"@esbuild/win32-x64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
"integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"dev": true,
"optional": true
},
@ -2606,9 +2613,9 @@
}
},
"@vitejs/plugin-vue": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
"integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
"dev": true,
"requires": {}
},
@ -2935,33 +2942,33 @@
}
},
"esbuild": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
"integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"requires": {
"@esbuild/android-arm": "0.17.19",
"@esbuild/android-arm64": "0.17.19",
"@esbuild/android-x64": "0.17.19",
"@esbuild/darwin-arm64": "0.17.19",
"@esbuild/darwin-x64": "0.17.19",
"@esbuild/freebsd-arm64": "0.17.19",
"@esbuild/freebsd-x64": "0.17.19",
"@esbuild/linux-arm": "0.17.19",
"@esbuild/linux-arm64": "0.17.19",
"@esbuild/linux-ia32": "0.17.19",
"@esbuild/linux-loong64": "0.17.19",
"@esbuild/linux-mips64el": "0.17.19",
"@esbuild/linux-ppc64": "0.17.19",
"@esbuild/linux-riscv64": "0.17.19",
"@esbuild/linux-s390x": "0.17.19",
"@esbuild/linux-x64": "0.17.19",
"@esbuild/netbsd-x64": "0.17.19",
"@esbuild/openbsd-x64": "0.17.19",
"@esbuild/sunos-x64": "0.17.19",
"@esbuild/win32-arm64": "0.17.19",
"@esbuild/win32-ia32": "0.17.19",
"@esbuild/win32-x64": "0.17.19"
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"escape-string-regexp": {
@ -3474,9 +3481,9 @@
"dev": true
},
"nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
},
"natural-compare": {
"version": "1.4.0",
@ -3585,11 +3592,11 @@
"dev": true
},
"postcss": {
"version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"requires": {
"nanoid": "^3.3.6",
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
@ -3659,9 +3666,9 @@
}
},
"rollup": {
"version": "3.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.0.tgz",
"integrity": "sha512-OgraHOIg2YpHQTjl0/ymWfFNBEyPucB7lmhXrQUh38qNOegxLapSPFs9sNr0qKR75awW41D93XafoR2QfhBdUQ==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
@ -3688,9 +3695,9 @@
}
},
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@ -3813,15 +3820,15 @@
"dev": true
},
"vite": {
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
"integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"requires": {
"esbuild": "^0.17.5",
"esbuild": "^0.18.10",
"fsevents": "~2.3.2",
"postcss": "^8.4.23",
"rollup": "^3.21.0"
"postcss": "^8.4.27",
"rollup": "^3.27.1"
}
},
"vue": {
@ -3872,9 +3879,9 @@
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true
},
"wrappy": {

View File

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

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

@ -18,7 +18,7 @@
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"
@ -51,7 +51,7 @@
dayNightSwitch: {
tooltip: "Jour/Nuit"
}
sunlight: "Soleil"
sunlight: "Lumière du Soleil"
ambientLight: "Lumière Ambiante"
}
resolution: {
@ -70,9 +70,9 @@
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é"
}
@ -84,7 +84,7 @@
contrast: "Contrasté"
}
debug: {
button: "Debug"
button: "Debogage"
}
resetAllSettings: {
button: "Réinitialiser tous les Paramètres"
@ -94,7 +94,7 @@
tooltip: "Liste des Joueurs"
}
compass: {
tooltip: "Boussole / Pointe le Nord"
tooltip: "Boussole / Pointer le Nord"
}
screenshot: {
title: "Capture d\'écran"
@ -122,7 +122,7 @@
blockTooltip: {
block: "Bloc"
position: "Position"
chunk: "Chunk"
chunk: "Tronçon"
region: {
region: "Région"
file: "Fichier"
@ -141,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

@ -83,6 +83,9 @@
light: "Licht"
contrast: "Contrast"
}
chunkBorders: {
button: "Laat chunk grenzen zien"
}
debug: {
button: "Debug"
}

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>

View File

@ -5,23 +5,23 @@
tooltip: "Menü"
}
map: {
unloaded: "Herhangi harita yüklenmedi."
unloaded: "Haritalar yüklenmedi."
loading: "Harita yükleniyor..."
errored: "Bu haritayı yüklerken bir hata oluştu!"
errored: "Haritayı yüklerken bir hata oluştu!"
}
maps: {
title: "Haritalar"
button: "Haritalar"
tooltip: "Harita-Listesi"
tooltip: "Harita listesi"
}
markers: {
title: "Noktalar"
button: "Noktalar"
tooltip: "Nokta-Listesi"
tooltip: "Nokta listesi"
marker: "nokta | noktalar"
markerSet: "nokta-kur | noktalar-kur"
markerSet: "nokta kur | noktalar kur"
searchPlaceholder: "Ara..."
followPlayerTitle: "Oyuncuyu Takip Et"
followPlayerTitle: "Oyuncuyu takip et"
sort {
title: "Sıralama türü"
by {
@ -36,23 +36,23 @@
button: "Ayarlar"
}
goFullscreen: {
button: "Tam Ekran"
button: "Tam ekran"
}
resetCamera: {
button: "Kamerayı Sıfırla"
tooltip: "Kamerayı & Pozisyonu Sıfırla"
button: "Kamerayı sıfırla"
tooltip: "Kamerayı ve pozisyonu sıfırla"
}
updateMap: {
button: "Haritayı Güncele"
tooltip: "Harita Parça Önbelleğini Temizle"
button: "Haritayı güncele"
tooltip: "Harita Parça önbelleğini temizle"
}
lighting: {
title: "Aydınlatma"
dayNightSwitch: {
tooltip: "Sabah/Akşam"
}
sunlight: "Güneş Işığı"
ambientLight: "Çevre-Işığı"
sunlight: "Güneş ışığı"
ambientLight: "Çevre ışığı"
}
resolution: {
title: "Çözünürlük"
@ -61,20 +61,20 @@
low: "Düşük (Upscaling x0.5)"
}
mapControls: {
title: "Harita Kontrolleri"
showZoomButtons: "Yakınlaştırma Tuşlarını Göster"
title: "Harita kontrolleri"
showZoomButtons: "Yakınlaştırma tuşlarını göster"
}
freeFlightControls: {
title: "Serbest Kamera Kontrolleri"
mouseSensitivity: "Fare-Hassasiyeti"
invertMouseY: "Fareyi Tersine Çevir"
title: "Serbest kamera kontrolleri"
mouseSensitivity: "Fare hassasiyeti"
invertMouseY: "Fareyi tersine çevir"
}
renderDistance: {
title: "Görüş Mesafesi"
hiresLayer: "Hires Katmanı"
lowersLayer: "Lowres Katmanı"
loadHiresWhileMoving: "Haraket ederken hires'i yüklemeye devam et"
off: "Kapalıl"
title: "Görüş mesafesi"
hiresLayer: "Yüksek çözünürlük katmanı"
lowersLayer: "Düşük çözünürlük Katmanı"
loadHiresWhileMoving: "Haraket ettirirken yüklemeye devam et"
off: "Kapalı"
}
theme: {
title: "Tema"
@ -84,36 +84,36 @@
contrast: "Kontrast"
}
debug: {
button: "Hata Ayıkla"
button: "Hata ayıklama"
}
resetAllSettings: {
button: "Tüm Ayarları Sıfırla"
button: "Tüm ayarları sıfırla"
}
players: {
title: "Oyuncular"
tooltip: "Oyuncu-Listesi"
tooltip: "Oyuncu listesi"
}
compass: {
tooltip: "Pusula / Kuzeye Dön"
}
screenshot: {
title: "Ekran Görüntüsü"
button: "Ekran Görüntüsü Al"
clipboard: "Panoya Kaydet"
title: "Ekran görüntüsü"
button: "Ekran görüntüsü Al"
clipboard: "Panoya kaydet"
}
controls: {
title: "Görüş / Kontroller"
perspective: {
button: "Perspektif"
tooltip: "Perspektif-Bakış"
tooltip: "Perspektif bakışı"
}
flatView: {
button: "Düz"
tooltip: "Ortografik / Düz-Bakış"
button: "Kuşbakışı"
tooltip: "Ortografik / Kuşbakışı"
}
freeFlight: {
button: "Serbest-Kamera"
tooltip: "Serbest-Kamera / İzleyici Modu"
button: "Serbest kamera"
tooltip: "Serbest kamera / Seyirci modu"
}
}
language: {
@ -139,32 +139,32 @@
content: """
<img src="assets/logo.png" style="display: block; width: 40%; margin: 3em auto; border-radius: 50%">
<p>
<h2>Fare-Kontrolleri:</h2>
<h2>Fare Kontrolleri:</h2>
<table>
<tr><th>tı</th><td><kbd>sol tık</kbd> + haraket ettir</td></tr>
<tr><th>yakınlaştır</th><td><kbd>fare tekerleği</kbd> (kaydır)</td></tr>
<tr><th>döndür / eğilt</th><td><kbd>sağ tık</kbd> + haraket ettir</td></tr>
<tr><th>Tı</th><td><kbd>sol tık</kbd> + haraket ettir</td></tr>
<tr><th>Yakınlaştır</th><td><kbd>fare tekerleği</kbd> (kaydır)</td></tr>
<tr><th>Döndür / Eğilt</th><td><kbd>sağ tık</kbd> + haraket ettir</td></tr>
</table>
</p>
<p>
<h2>Klavye-Kontrolleri:</h2>
<h2>Klavye Kontrolleri:</h2>
<table>
<tr><th>tı</th><td><kbd>wasd</kbd> / <kbd>ok tuşları</kbd></td></tr>
<tr><th>yakınlaştır</th><td>Numpad: <kbd>+</kbd>/<kbd>-</kbd> veya <kbd>Ins</kbd>/<kbd>Home</kbd></td></tr>
<tr><th>döndür / eğilt</th><td><kbd>Left-Alt</kbd> + <kbd>wasd</kbd> / <kbd>arrow-keys</kbd> veya <kbd>Delete</kbd>/<kbd>End</kbd>/<kbd>Page Up</kbd>/<kbd>Page Down</kbd></td></tr>
<tr><th>Tı</th><td><kbd>wasd</kbd> / <kbd>ok tuşları</kbd></td></tr>
<tr><th>Yakınlaştır</th><td>Numpad: <kbd>+</kbd>/<kbd>-</kbd> veya <kbd>Ins</kbd>/<kbd>Home</kbd></td></tr>
<tr><th>Döndür / Eğim ver</th><td><kbd>Left-Alt</kbd> + <kbd>wasd</kbd> / <kbd>ok tuşları</kbd> veya <kbd>Delete</kbd>/<kbd>End</kbd>/<kbd>Page Up</kbd>/<kbd>Page Down</kbd></td></tr>
</table>
</p>
<p>
<h2>Dokunmatik-Kontrolleri:</h2>
<h2>Dokunmatik Kontrolleri:</h2>
<table>
<tr><th>tı</th><td>dokun + kaydır</td></tr>
<tr><th>yakınlaştır</th><td>iki parmağını birbirine yaklaştır/uzaklaştır</td></tr>
<tr><th>döndür / eğilt</th><td>iki parmakla dokun + haraket ettir / aşşağı/yukarı kaydır</td></tr>
<tr><th>Tı</th><td>dokun + kaydır</td></tr>
<tr><th>Yakınlaştır</th><td>iki parmaklarını birbirine yaklaştır/uzaklaştır</td></tr>
<tr><th>Döndür / Eğim ver</th><td>iki parmağınla dokun + haraket ettir / aşağı/yukarı kaydır</td></tr>
</table>
</p>
<br><hr>
<p class="info-footer">
Bu harita &#9829; using <a href="https://bluecolo.red/bluemap">BlueMap</a> {version} ile oluşturulmuştur
Bu harita &#9829; <a href="https://bluecolo.red/bluemap">BlueMap</a> {version} ile oluşturulmuştur
</p>
"""
}

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: "Налаштування"
@ -26,8 +39,8 @@
button: "Перейти в повноекранний режим"
}
resetCamera: {
button: "Скинути камеру"
tooltip: "Скинути камеру та позицію"
button: "Скинути налаштування камери"
tooltip: "Скинути налаштування та позицію камери"
}
updateMap: {
button: "Оновити карту"
@ -70,6 +83,9 @@
light: "Світла"
contrast: "Контрастна"
}
chunkBorders: {
button: "Показувати межі чанків"
}
debug: {
button: "Відлагоджувальний режим"
}
@ -117,7 +133,7 @@
light: {
light: "Освітлення"
sun: "Сонце"
block: "Блок"
block: "Блоки"
}
}
info: {

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: "更新地图"
@ -47,15 +60,21 @@
normal: "普通(默认 x1"
low: "低(粗糙 x0.5"
}
mapControls: {
title: "地图控制"
showZoomButtons: "显示缩放按钮"
}
freeFlightControls: {
title: "自由飞行控制"
mouseSensitivity: "鼠标灵敏度"
invertMouseY: "反转鼠标 Y"
invertMouseY: "反转鼠标 Y"
}
renderDistance: {
title: "渲染距离"
hiresLayer: "高分辨率"
lowersLayer: "低分辨率"
loadHiresWhileMoving: "移动时加载高清图像"
off: "关闭"
}
theme: {
title: "主题"
@ -75,7 +94,7 @@
tooltip: "玩家列表"
}
compass: {
tooltip: "罗盘 / 朝北"
tooltip: "指南针 / 朝北"
}
screenshot: {
title: "截图"
@ -122,9 +141,9 @@
<p>
<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>
<tr><th>缩放</th><td><kbd>鼠标滚轮</kbd>(滚动)</td></tr>
<tr><th>旋转 / 倾斜</th><td><kbd>右键点击</kbd> + 拖动</td></tr>
</table>
</p>
<p>
@ -138,8 +157,8 @@
<p>
<h2>触屏控制:</h2>
<table>
<tr><th>移动</th><td>触屏 + 拖</td></tr>
<tr><th>缩放</th><td>双指触屏 + 滑动</td></tr>
<tr><th>移动</th><td>触屏 + 拖</td></tr>
<tr><th>缩放</th><td>双指触屏 + 捏合</td></tr>
<tr><th>旋转 / 倾斜</th><td>双指触屏 + 旋转 / 向上滑动 / 向下滑动</td></tr>
</table>
</p>

View File

@ -9,30 +9,27 @@ $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 !!!
// compression
$compressionHeaderMap = [
"bluemap:none" => null,
"bluemap:gzip" => "gzip",
"bluemap:deflate" => "deflate",
"bluemap:zstd" => "zstd",
"bluemap:lz4" => "lz4"
];
// 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;
}
// meta files
$metaFileKeys = [
"settings.json" => "bluemap:settings",
"textures.json" => "bluemap:textures",
"live/markers.json" => "bluemap:markers",
"live/players.json" => "bluemap:players",
];
// mime-types for meta-files
$mimeDefault = "application/octet-stream";
@ -70,6 +67,34 @@ $mimeTypes = [
"woff2" => "font/woff2"
];
// 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;
}
function issetOrElse(& $var, $fallback) {
return isset($var) ? $var : $fallback;
}
function compressionHeader($compressionKey) {
global $compressionHeaderMap;
$compressionHeader = issetOrElse($compressionHeaderMap[$compressionKey], null);
if ($compressionHeader)
header("Content-Encoding: ".$compressionHeader);
}
function getMimeType($path) {
global $mimeDefault, $mimeTypes;
@ -100,7 +125,7 @@ if ($root === "/" || $root === "\\") $root = "";
$uriPath = $_SERVER['REQUEST_URI'];
$path = substr($uriPath, strlen($root));
// add /
// add /
if ($path === "") {
header("Location: $uriPath/");
exit;
@ -122,88 +147,101 @@ if (startsWith($path, "/maps/")) {
// Initialize PDO
try {
$sql = new PDO("$driver:host=$hostname;dbname=$database", $username, $password);
$sql = new PDO("$driver:host=$hostname;port=$port;dbname=$database", $username, $password);
$sql->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e ) { 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]);
$storage = $lod === 0 ? "bluemap:hires" : "bluemap:lowres/".$lod;
$tileX = intval(str_replace("/", "", $matches[2][0]));
$tileZ = intval(str_replace("/", "", $matches[3][0]));
$compression = $lod === 0 ? $hiresCompression : "none";
// query for tile
try {
$statement = $sql->prepare("
SELECT t.data
FROM bluemap_map_tile t
SELECT d.data, c.key
FROM bluemap_grid_storage_data d
INNER JOIN bluemap_map m
ON t.map = m.id
INNER JOIN bluemap_map_tile_compression c
ON t.compression = c.id
ON d.map = m.id
INNER JOIN bluemap_grid_storage s
ON d.storage = s.id
INNER JOIN bluemap_compression c
ON d.compression = c.id
WHERE m.map_id = :map_id
AND t.lod = :lod
AND t.x = :x
AND t.z = :z
AND c.compression = :compression
AND s.key = :storage
AND d.x = :x
AND d.z = :z
");
$statement->bindParam( ':map_id', $mapId, PDO::PARAM_STR );
$statement->bindParam( ':lod', $lod, PDO::PARAM_INT );
$statement->bindParam( ':storage', $storage, PDO::PARAM_STR );
$statement->bindParam( ':x', $tileX, PDO::PARAM_INT );
$statement->bindParam( ':z', $tileZ, PDO::PARAM_INT );
$statement->bindParam( ':compression', $compression, PDO::PARAM_STR);
$statement->setFetchMode(PDO::FETCH_ASSOC);
$statement->execute();
// return result
if ($line = $statement->fetch()) {
if ($compression !== "none")
header("Content-Encoding: $compression");
header("Cache-Control: public,max-age=86400");
compressionHeader($line["key"]);
if ($lod === 0) {
header("Content-Type: application/json");
header("Content-Type: application/octet-stream");
} else {
header("Content-Type: image/png");
}
send($line["data"]);
exit;
}
} catch (PDOException $e) { error(500, "Failed to fetch data"); }
} catch (PDOException $e) { error(500, "Failed to fetch data"); }
// empty json response if nothing found
header("Content-Type: application/json");
echo "{}";
// no content if nothing found
http_response_code(204);
exit;
}
// provide meta-files
try {
$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 = :map_id
AND t.key = :map_path
");
$statement->bindParam( ':map_id', $mapId, PDO::PARAM_STR );
$statement->bindParam( ':map_path', $mapPath, PDO::PARAM_STR );
$statement->setFetchMode(PDO::FETCH_ASSOC);
$statement->execute();
$storage = issetOrElse($metaFileKeys[$mapPath], null);
if ($storage === null && startsWith($mapPath, "assets/"))
$storage = "bluemap:asset/".substr($mapPath, strlen("assets/"));
if ($line = $statement->fetch()) {
header("Content-Type: ".getMimeType($mapPath));
send($line["value"]);
exit;
}
} catch (PDOException $e) { error(500, "Failed to fetch data"); }
if ($storage !== null) {
try {
$statement = $sql->prepare("
SELECT d.data, c.key
FROM bluemap_item_storage_data d
INNER JOIN bluemap_map m
ON d.map = m.id
INNER JOIN bluemap_item_storage s
ON d.storage = s.id
INNER JOIN bluemap_compression c
ON d.compression = c.id
WHERE m.map_id = :map_id
AND s.key = :storage
");
$statement->bindParam( ':map_id', $mapId, PDO::PARAM_STR );
$statement->bindParam( ':storage', $storage, PDO::PARAM_STR );
$statement->setFetchMode(PDO::FETCH_ASSOC);
$statement->execute();
if ($line = $statement->fetch()) {
header("Cache-Control: public,max-age=86400");
header("Content-Type: ".getMimeType($mapPath));
compressionHeader($line["key"]);
send($line["data"]);
exit;
}
} catch (PDOException $e) { error(500, "Failed to fetch data"); }
}
}
// no match => 404
error(404);
error(404);

View File

@ -6,9 +6,9 @@
<SwitchHandle :on="markerSet.visible" v-if="markerSet.toggleable"/>
</div>
<div class="stats">
<div v-if="markerSet.markers.length > 0">
{{ markerSet.markers.length }}
{{ $t('markers.marker', markerSet.markers.length) }}
<div v-if="filteredMarkerCount > 0">
{{ filteredMarkerCount }}
{{ $t('markers.marker', filteredMarkerCount) }}
</div>
<div v-if="filteredMarkerSetCount > 0">
{{ filteredMarkerSetCount }}
@ -46,6 +46,13 @@ export default {
}
return count;
},
filteredMarkerCount() {
let count = 0;
for (let marker of this.markerSet.markers) {
if (marker.listed) count++;
}
return count;
},
label() {
if (this.markerSet.id === "bm-players") return this.$t("players.title");
return this.markerSet.label;

View File

@ -82,9 +82,13 @@ export default {
openMore(markerSet) {
this.menu.openPage(
this.menu.currentPage().id,
this.menu.currentPage().title + " > " + markerSet.label,
this.menu.currentPage().title + " > " + this.labelOf(markerSet),
{markerSet: markerSet}
)
},
labelOf(markerSet) {
if (markerSet.id === "bm-players") return this.$t("players.title");
return markerSet.label;
}
}
}

View File

@ -56,6 +56,8 @@
>{{lang.name}}</SimpleButton>
</Group>
<SwitchButton :on="mapViewer.uniforms.chunkBorders.value" @action="switchChunkBorders(); $bluemap.saveUserSettings();">{{ $t("chunkBorders.button") }}</SwitchButton>
<SwitchButton :on="appState.debug" @action="switchDebug(); $bluemap.saveUserSettings();">{{ $t("debug.button") }}</SwitchButton>
<SimpleButton @action="$bluemap.resetSettings()">{{ $t("resetAllSettings.button") }}</SimpleButton>
@ -105,6 +107,9 @@ name: "SettingsMenu",
}
},
methods: {
switchChunkBorders() {
this.$bluemap.setChunkBorders(!this.mapViewer.uniforms.chunkBorders.value);
},
switchDebug() {
this.$bluemap.setDebug(!this.appState.debug);
},
@ -121,4 +126,4 @@ name: "SettingsMenu",
<style>
</style>
</style>

View File

@ -146,6 +146,7 @@ export class BlueMapApp {
let styleElement = document.createElement("link");
styleElement.rel = "stylesheet";
styleElement.href = styleUrl;
alert(this.events, "Loading style: " + styleUrl, "fine");
document.head.appendChild(styleElement);
}
@ -153,6 +154,9 @@ export class BlueMapApp {
await this.mapViewer.switchMap(null);
oldMaps.forEach(map => map.dispose());
// load user settings
await this.loadUserSettings();
// load maps
this.maps = await this.loadMaps();
for (let map of this.maps) {
@ -179,9 +183,6 @@ export class BlueMapApp {
if(this.updateLoop) clearTimeout(this.updateLoop);
this.updateLoop = setTimeout(this.update, 1000);
// load user settings
await this.loadUserSettings();
// save user settings
this.saveUserSettings();
@ -189,6 +190,7 @@ export class BlueMapApp {
if (this.settings.scripts) for (let scriptUrl of this.settings.scripts) {
let scriptElement = document.createElement("script");
scriptElement.src = scriptUrl;
alert(this.events, "Loading script: " + scriptUrl, "fine");
document.body.appendChild(scriptElement);
}
}
@ -297,15 +299,17 @@ export class BlueMapApp {
// create maps
if (settings.maps !== undefined){
for (let mapId of settings.maps) {
let loadingPromises = settings.maps.map(mapId => {
let map = new BlueMapMap(mapId, this.dataUrl + mapId + "/", this.loadBlocker, this.mapViewer.events);
maps.push(map);
await map.loadSettings()
return map.loadSettings(this.mapViewer.tileCacheHash)
.catch(error => {
alert(this.events, `Failed to load settings for map '${map.data.id}':` + error, "warning");
});
}
})
await Promise.all(loadingPromises);
}
// sort maps
@ -521,6 +525,10 @@ export class BlueMapApp {
this.appState.controls.state = "free";
}
setChunkBorders(chunkBorders) {
this.mapViewer.data.uniforms.chunkBorders.value = chunkBorders;
}
setDebug(debug) {
this.appState.debug = debug;
@ -604,6 +612,7 @@ export class BlueMapApp {
this.setTheme(this.loadUserSetting("theme", this.appState.theme));
this.setScreenshotClipboard(this.loadUserSetting("screenshotClipboard", this.appState.screenshot.clipboard));
await setLanguage(this.loadUserSetting("lang", i18n.locale.value));
this.setChunkBorders(this.loadUserSetting("chunkBorders", this.mapViewer.data.uniforms.chunkBorders.value))
this.setDebug(this.loadUserSetting("debug", this.appState.debug));
alert(this.events, "Settings loaded!", "info");
@ -625,6 +634,7 @@ export class BlueMapApp {
this.saveUserSetting("theme", this.appState.theme);
this.saveUserSetting("screenshotClipboard", this.appState.screenshot.clipboard);
this.saveUserSetting("lang", i18n.locale.value);
this.saveUserSetting("chunkBorders", this.mapViewer.data.uniforms.chunkBorders.value);
this.saveUserSetting("debug", this.appState.debug);
alert(this.events, "Settings saved!", "info");
@ -724,6 +734,7 @@ export class BlueMapApp {
controls.ortho = parseFloat(values[8]);
this.updatePageAddress();
this.mapViewer.updateLoadedMapArea();
return true;
}

View File

@ -61,6 +61,7 @@ export class MapViewer {
ambientLight: { value: 0 },
skyColor: { value: new Color(0.5, 0.5, 1) },
voidColor: { value: new Color(0, 0, 0) },
chunkBorders: { value: false },
hiresTileMap: {
value: {
map: null,
@ -179,8 +180,8 @@ export class MapViewer {
handleMapInteraction(screenPosition, data = {}) {
let rootOffset = elementOffset(this.rootElement);
let normalizedScreenPos = new Vector2(
((screenPosition.x - rootOffset.top) / this.rootElement.clientWidth) * 2 - 1,
-((screenPosition.y - rootOffset.left) / this.rootElement.clientHeight) * 2 + 1
((screenPosition.x - rootOffset.left) / this.rootElement.clientWidth) * 2 - 1,
-((screenPosition.y - rootOffset.top) / this.rootElement.clientHeight) * 2 + 1
);
if (this.map && this.map.isLoaded){
@ -294,7 +295,7 @@ export class MapViewer {
}
// render
if (delta >= 1000 || Date.now() - this.lastRedrawChange < 1000) {
if (delta >= 50 || Date.now() - this.lastRedrawChange < 1000) {
this.lastFrame = now;
this.render(delta);
}
@ -325,6 +326,8 @@ export class MapViewer {
if (this.map && this.map.isLoaded) {
this.map.animations.forEach(animation => animation.step(delta))
// shift whole scene including camera towards 0,0 to tackle shader-precision issues
const s = 10000;
const sX = Math.round(this.camera.position.x / s) * s;

View File

@ -43,6 +43,7 @@ export class PopupMarker extends Marker {
this.elementObject = new CSS2DObject(htmlToElement(`<div id="bm-marker-${this.data.id}" class="bm-marker-${this.data.type}">Test</div>`));
this.elementObject.position.set(0.5, 1, 0.5);
this.elementObject.disableDepthTest = true;
this.addEventListener( 'removed', () => {
if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
});

View File

@ -58,8 +58,8 @@ export class ControlsManager {
this.lastOrtho = this.ortho;
this.lastTilt = this.tilt;
this.lastMapUpdatePosition = this.position.clone();
this.lastMapUpdateDistance = this.distance;
this.lastMapUpdatePosition = null;
this.lastMapUpdateDistance = null;
this.averageDeltaTime = 16;
@ -157,9 +157,11 @@ export class ControlsManager {
}
if (
this.lastMapUpdatePosition === null ||
this.lastMapUpdateDistance === null ||
Math.abs(this.lastMapUpdatePosition.x - this.position.x) >= triggerDistance ||
Math.abs(this.lastMapUpdatePosition.z - this.position.z) >= triggerDistance ||
(this.distance < 1000 && this.lastMapUpdateDistance > 1000)
(this.distance < 1000 && this.lastMapUpdateDistance >= 1000)
) {
this.lastMapUpdatePosition = this.position.clone();
this.lastMapUpdateDistance = this.distance;

View File

@ -39,6 +39,7 @@ import {TileManager} from "./TileManager";
import {TileLoader} from "./TileLoader";
import {LowresTileLoader} from "./LowresTileLoader";
import {reactive} from "vue";
import {TextureAnimation} from "@/js/map/TextureAnimation";
export class Map {
@ -86,6 +87,9 @@ export class Map {
/** @type {Texture[]} */
this.loadedTextures = [];
/** @type {TextureAnimation[]} */
this.animations = [];
/** @type {TileManager} */
this.hiresTileManager = null;
/** @type {TileManager[]} */
@ -105,8 +109,8 @@ export class Map {
load(hiresVertexShader, hiresFragmentShader, lowresVertexShader, lowresFragmentShader, uniforms, tileCacheHash = 0) {
this.unload()
let settingsPromise = this.loadSettings();
let textureFilePromise = this.loadTexturesFile();
let settingsPromise = this.loadSettings(tileCacheHash);
let textureFilePromise = this.loadTexturesFile(tileCacheHash);
this.lowresMaterial = this.createLowresMaterial(lowresVertexShader, lowresFragmentShader, uniforms);
@ -134,8 +138,8 @@ export class Map {
* Loads the settings of this map
* @returns {Promise<void>}
*/
loadSettings() {
return this.loadSettingsFile()
loadSettings(tileCacheHash) {
return this.loadSettingsFile(tileCacheHash)
.then(worldSettings => {
this.data.name = worldSettings.name ? worldSettings.name : this.data.name;
@ -223,13 +227,13 @@ export class Map {
* Loads the settings.json file for this map
* @returns {Promise<Object>}
*/
loadSettingsFile() {
loadSettingsFile(tileCacheHash) {
return new Promise((resolve, reject) => {
alert(this.events, `Loading settings for map '${this.data.id}'...`, "fine");
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.data.settingsUrl + "?" + generateCacheHash(),
loader.load(this.data.settingsUrl + "?" + tileCacheHash,
resolve,
() => {},
() => reject(`Failed to load the settings.json for map: ${this.data.id}`)
@ -241,13 +245,13 @@ export class Map {
* Loads the textures.json file for this map
* @returns {Promise<Object>}
*/
loadTexturesFile() {
loadTexturesFile(tileCacheHash) {
return new Promise((resolve, reject) => {
alert(this.events, `Loading textures for map '${this.data.id}'...`, "fine");
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.data.texturesUrl + "?" + generateCacheHash(),
loader.load(this.data.texturesUrl + "?" + tileCacheHash,
resolve,
() => {},
() => reject(`Failed to load the textures.json for map: ${this.data.id}`)
@ -264,7 +268,8 @@ export class Map {
* resourcePath: string,
* color: number[],
* halfTransparent: boolean,
* texture: string
* texture: string,
* animation: any | undefined
* }[]} the textures-data
* @returns {ShaderMaterial[]} the hires Material (array because its a multi-material)
*/
@ -293,7 +298,24 @@ export class Map {
texture.wrapT = ClampToEdgeWrapping;
texture.flipY = false;
texture.flatShading = true;
texture.image.addEventListener("load", () => texture.needsUpdate = true);
let animationUniforms = {
animationFrameHeight: { value: 1 },
animationFrameIndex: { value: 0 },
animationInterpolationFrameIndex: { value: 0 },
animationInterpolation: { value: 0 }
};
let animation = null;
if (textureSettings.animation) {
animation = new TextureAnimation(animationUniforms, textureSettings.animation);
this.animations.push(animation);
}
texture.image.addEventListener("load", () => {
texture.needsUpdate = true
if (animation) animation.init(texture.image.naturalWidth, texture.image.naturalHeight)
});
this.loadedTextures.push(texture);
@ -304,7 +326,8 @@ export class Map {
type: 't',
value: texture
},
transparent: { value: transparent }
transparent: { value: transparent },
...animationUniforms
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
@ -363,6 +386,8 @@ export class Map {
this.loadedTextures.forEach(texture => texture.dispose());
this.loadedTextures = [];
this.animations = [];
}
/**

View File

@ -0,0 +1,85 @@
export class TextureAnimation {
/**
* @param uniforms {{
* animationFrameHeight: { value: number },
* animationFrameIndex: { value: number },
* animationInterpolationFrameIndex: { value: number },
* animationInterpolation: { value: number }
* }}
* @param data {{
* interpolate: boolean,
* width: number,
* height: number,
* frametime: number,
* frames: {
* index: number,
* time: number
* }[] | undefined
* }}
*/
constructor(uniforms, data) {
this.uniforms = uniforms;
this.data = {
interpolate: false,
width: 1,
height: 1,
frametime: 1,
...data
};
this.frameImages = 1;
this.frameDelta = 0;
this.frameTime = this.data.frametime * 50;
this.frames = 1;
this.frameIndex = 0;
}
/**
* @param width {number}
* @param height {number}
*/
init(width, height) {
this.frameImages = height / width;
this.uniforms.animationFrameHeight.value = 1 / this.frameImages;
this.frames = this.frameImages;
if (this.data.frames && this.data.frames.length > 0) {
this.frames = this.data.frames.length;
} else {
this.data.frames = null;
}
}
/**
* @param delta {number}
*/
step(delta) {
this.frameDelta += delta;
if (this.frameDelta > this.frameTime) {
this.frameDelta -= this.frameTime;
this.frameDelta %= this.frameTime;
this.frameIndex++;
this.frameIndex %= this.frames;
if (this.data.frames) {
let frame = this.data.frames[this.frameIndex]
let nextFrame = this.data.frames[(this.frameIndex + 1) % this.frames];
this.uniforms.animationFrameIndex.value = frame.index;
this.uniforms.animationInterpolationFrameIndex.value = nextFrame.index;
this.frameTime = frame.time * 50;
} else {
this.uniforms.animationFrameIndex.value = this.frameIndex;
this.uniforms.animationInterpolationFrameIndex.value = (this.frameIndex + 1) % this.frames;
}
}
if (this.data.interpolate) {
this.uniforms.animationInterpolation.value = this.frameDelta / this.frameTime;
}
}
}

View File

@ -23,13 +23,14 @@
* THE SOFTWARE.
*/
import {pathFromCoords} from "../util/Utils";
import {BufferGeometryLoader, FileLoader, Mesh} from "three";
import {BufferGeometryLoader, FileLoader, Mesh, Material} from "three";
import {PRBMLoader} from "./hires/PRBMLoader";
export class TileLoader {
/**
* @param tilePath {string}
* @param material {THREE.Material | THREE.Material[]}
* @param material {Material | Material[]}
* @param tileSettings {{
* tileSize: {x: number, z: number},
* scale: {x: number, z: number},
@ -50,23 +51,17 @@ export class TileLoader {
this.loadBlocker = loadBlocker;
this.fileLoader = new FileLoader();
this.fileLoader.setResponseType('json');
this.fileLoader.setResponseType('arraybuffer');
this.bufferGeometryLoader = new BufferGeometryLoader();
this.bufferGeometryLoader = new PRBMLoader();
}
load = (tileX, tileZ, cancelCheck = () => false) => {
let tileUrl = this.tilePath + pathFromCoords(tileX, tileZ) + '.json';
let tileUrl = this.tilePath + pathFromCoords(tileX, tileZ) + '.prbm';
//await this.loadBlocker();
return new Promise((resolve, reject) => {
this.fileLoader.load(tileUrl + '?' + this.tileCacheHash,
async json => {
let geometryJson = json.tileGeometry || {};
if (!geometryJson.type || geometryJson.type !== 'BufferGeometry'){
reject({status: "empty"});
return;
}
async data => {
await this.loadBlocker();
if (cancelCheck()){
@ -74,7 +69,7 @@ export class TileLoader {
return;
}
let geometry = this.bufferGeometryLoader.parse(geometryJson);
let geometry = this.bufferGeometryLoader.parse(data);
let object = new Mesh(geometry, this.material);

View File

@ -24,6 +24,7 @@
*/
import { ShaderChunk } from 'three';
// language=GLSL
export const HIRES_FRAGMENT_SHADER = `
${ShaderChunk.logdepthbuf_pars_fragment}
@ -31,12 +32,18 @@ ${ShaderChunk.logdepthbuf_pars_fragment}
#define texture texture2D
#endif
uniform float distance;
uniform sampler2D textureImage;
uniform float sunlightStrength;
uniform float ambientLight;
uniform float animationFrameHeight;
uniform float animationFrameIndex;
uniform float animationInterpolationFrameIndex;
uniform float animationInterpolation;
uniform bool chunkBorders;
varying vec3 vPosition;
//varying vec3 vWorldPosition;
varying vec3 vWorldPosition;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vColor;
@ -46,7 +53,12 @@ varying float vBlocklight;
//varying float vDistance;
void main() {
vec4 color = texture(textureImage, vUv);
vec4 color = texture(textureImage, vec2(vUv.x, animationFrameHeight * (vUv.y + animationFrameIndex)));
if (animationInterpolation > 0.0) {
color = mix(color, texture(textureImage, vec2(vUv.x, animationFrameHeight * (vUv.y + animationInterpolationFrameIndex))), animationInterpolation);
}
if (color.a <= 0.01) discard;
//apply vertex-color
@ -59,6 +71,25 @@ void main() {
float light = mix(vBlocklight, max(vSunlight, vBlocklight), sunlightStrength);
color.rgb *= mix(ambientLight, 1.0, light / 15.0);
if (chunkBorders) {
vec4 lineColour = vec4(1.0, 0.0, 1.0, 0.4);
float lineInterval = 16.0;
float lineThickness = 0.125; //width of two Minecraft pixels
float offset = 0.5;
vec2 worldPos = vWorldPosition.xz;
worldPos += offset;
float x = abs(mod(worldPos.x, lineInterval) - offset);
float y = abs(mod(worldPos.y, lineInterval) - offset);
bool isChunkBorder = x < lineThickness || y < lineThickness;
//only show line on upwards facing surfaces
bool showChunkBorder = isChunkBorder && vNormal.y > 0.1;
float distFac = smoothstep(200.0, 600.0, distance);
color.rgb = mix(mix(color.rgb, lineColour.rgb, float(showChunkBorder) * lineColour.a), color.rgb, distFac);
}
gl_FragColor = color;
${ShaderChunk.logdepthbuf_fragment}

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