Compare commits

...

87 Commits
v4.0 ... master

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
1104 changed files with 15953 additions and 16785 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 6cad751ac59286a516007799bcad3f2868e0a802
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,22 +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.30")
compileOnly ("org.projectlombok:lombok:1.18.32")
annotationProcessor ("org.projectlombok:lombok:1.18.30")
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")
@ -72,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"))
@ -91,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")
}
@ -102,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()
@ -109,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,7 +26,6 @@ package de.bluecolored.bluemap.common;
import de.bluecolored.bluemap.common.config.*;
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import de.bluecolored.bluemap.core.MinecraftVersion;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
@ -34,7 +33,7 @@ import java.util.Map;
public interface BlueMapConfiguration {
MinecraftVersion getMinecraftVersion();
@Nullable String getMinecraftVersion();
CoreConfig getCoreConfig();
@ -48,7 +47,7 @@ public interface BlueMapConfiguration {
Map<String, StorageConfig> getStorageConfigs();
@Nullable Path getResourcePacksFolder();
@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.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.resources.datapack.DataPack;
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 de.bluecolored.bluemap.core.world.mca.MCAWorld;
import org.apache.commons.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
@ -68,12 +67,12 @@ 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 BlueMapConfiguration config;
private final WebFilesManager webFilesManager;
private MinecraftVersion minecraftVersion;
private ResourcePack resourcePack;
private final Map<String, World> worlds;
private final Map<String, BmMap> maps;
@ -200,7 +199,7 @@ public class BlueMapService implements Closeable {
dimension = DataPack.DIMENSION_THE_END;
} else if (
worldFolder.getNameCount() > 3 &&
worldFolder.getName(worldFolder.getNameCount() - 3).equals(Path.of("dimensions"))
worldFolder.getName(worldFolder.getNameCount() - 3).toString().equals("dimensions")
) {
String namespace = worldFolder.getName(worldFolder.getNameCount() - 2).toString();
String value = worldFolder.getName(worldFolder.getNameCount() - 1).toString();
@ -220,12 +219,12 @@ public class BlueMapService implements Closeable {
"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 = MCAWorld.id(worldFolder, dimension);
String worldId = World.id(worldFolder, dimension);
World world = worlds.get(worldId);
if (world == null) {
try {
Logger.global.logDebug("Loading world " + worldId + " ...");
world = MCAWorld.load(worldFolder, dimension);
world = MCAWorld.load(worldFolder, dimension, loadDataPack(worldFolder));
worlds.put(worldId, world);
} catch (IOException ex) {
throw new ConfigurationException(
@ -244,7 +243,7 @@ public class BlueMapService implements Closeable {
id,
name,
world,
storage,
storage.map(id),
getOrLoadResourcePack(),
mapConfig
);
@ -287,7 +286,7 @@ public class BlueMapService implements Closeable {
"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();
@ -320,108 +319,17 @@ public class BlueMapService implements Closeable {
public synchronized ResourcePack getOrLoadResourcePack() throws ConfigurationException, InterruptedException {
if (resourcePack == null) {
MinecraftVersion minecraftVersion = config.getMinecraftVersion();
@Nullable Path resourcePackFolder = config.getResourcePacksFolder();
@Nullable Path modsFolder = config.getModsFolder();
Path defaultResourceFile = config.getCoreConfig().getData().resolve("minecraft-client-" + minecraftVersion.getResource().getVersion().getVersionString() + ".jar");
Path resourceExtensionsFile = config.getCoreConfig().getData().resolve("resourceExtensions.zip");
try {
FileHelper.createDirectories(resourcePackFolder);
} catch (IOException ex) {
throw new ConfigurationException(
"BlueMap failed to create this folder:\n" +
resourcePackFolder + "\n" +
"Does BlueMap have sufficient permissions?",
ex);
}
MinecraftVersion minecraftVersion = getOrLoadMinecraftVersion();
Path vanillaResourcePack = minecraftVersion.getResourcePack();
if (Thread.interrupted()) throw new InterruptedException();
if (!Files.exists(defaultResourceFile)) {
if (config.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();
}
}
if (Thread.interrupted()) throw new InterruptedException();
Deque<Path> packRoots = getPackRoots();
packRoots.addLast(vanillaResourcePack);
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);
}
if (Thread.interrupted()) throw new InterruptedException();
try {
ResourcePack resourcePack = new ResourcePack();
List<Path> resourcePackRoots = new ArrayList<>();
if (resourcePackFolder != null) {
// load from resourcepack folder
try (Stream<Path> resourcepackFiles = Files.list(resourcePackFolder)) {
resourcepackFiles
.sorted(Comparator.reverseOrder())
.forEach(resourcePackRoots::add);
}
}
if (config.getCoreConfig().isScanForModResources()) {
// load from mods folder
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" +
@ -432,17 +340,125 @@ public class BlueMapService implements Closeable {
return this.resourcePack;
}
private Collection<Path> getWorldFolders() {
Set<Path> folders = new HashSet<>();
for (MapConfig mapConfig : config.getMapConfigs().values()) {
Path folder = mapConfig.getWorld();
if (folder == null) continue;
folder = folder.toAbsolutePath().normalize();
if (Files.isDirectory(folder)) {
folders.add(folder);
public synchronized DataPack loadDataPack(Path worldFolder) throws ConfigurationException, InterruptedException {
MinecraftVersion minecraftVersion = getOrLoadMinecraftVersion();
Path vanillaDataPack = minecraftVersion.getDataPack();
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);
}
}
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() {

View File

@ -32,21 +32,15 @@ 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.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class WebFilesManager {
@ -114,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"})

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,27 +29,44 @@ import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.api.BlueMapAPI;
import de.bluecolored.bluemap.api.BlueMapMap;
import de.bluecolored.bluemap.api.BlueMapWorld;
import de.bluecolored.bluemap.common.BlueMapService;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.world.World;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
public class BlueMapAPIImpl extends BlueMapAPI {
private final Plugin plugin;
private final BlueMapService blueMapService;
private final @Nullable Plugin plugin;
private final WebAppImpl webAppImpl;
private final @Nullable RenderManagerImpl renderManagerImpl;
private final @Nullable PluginImpl pluginImpl;
private final LoadingCache<Object, Optional<BlueMapWorld>> worldCache;
private final LoadingCache<String, Optional<BlueMapMap>> mapCache;
public BlueMapAPIImpl(Plugin plugin) {
this(plugin.getBlueMap(), plugin);
}
public BlueMapAPIImpl(BlueMapService blueMapService, @Nullable Plugin plugin) {
this.blueMapService = blueMapService;
this.plugin = plugin;
this.renderManagerImpl = plugin != null ? new RenderManagerImpl(this, plugin) : null;
this.webAppImpl = new WebAppImpl(blueMapService, plugin);
this.pluginImpl = plugin != null ? new PluginImpl(plugin) : null;
this.worldCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.weakKeys()
@ -60,24 +77,9 @@ 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.getBlueMap().getMaps();
Map<String, BmMap> maps = blueMapService.getMaps();
return maps.keySet().stream()
.map(this::getMap)
.filter(Optional::isPresent)
@ -87,7 +89,7 @@ public class BlueMapAPIImpl extends BlueMapAPI {
@Override
public Collection<BlueMapWorld> getWorlds() {
Map<String, World> worlds = plugin.getBlueMap().getWorlds();
Map<String, World> worlds = blueMapService.getWorlds();
return worlds.keySet().stream()
.map(this::getWorld)
.filter(Optional::isPresent)
@ -103,22 +105,24 @@ public class BlueMapAPIImpl extends BlueMapAPI {
public Optional<BlueMapWorld> getWorldUncached(Object world) {
if (world instanceof String) {
var coreWorld = plugin.getBlueMap().getWorlds().get(world);
var coreWorld = blueMapService.getWorlds().get(world);
if (coreWorld != null) world = coreWorld;
}
if (world instanceof World) {
var coreWorld = (World) world;
return Optional.of(new BlueMapWorldImpl(plugin, coreWorld));
if (world instanceof World coreWorld) {
return Optional.of(new BlueMapWorldImpl(coreWorld, blueMapService, plugin));
}
if (plugin == null) return Optional.empty();
ServerWorld serverWorld = plugin.getServerInterface().getServerWorld(world).orElse(null);
if (serverWorld == null) return Optional.empty();
World coreWorld = plugin.getWorld(serverWorld);
if (coreWorld == null) return Optional.empty();
return Optional.of(new BlueMapWorldImpl(plugin, coreWorld));
return Optional.of(new BlueMapWorldImpl(coreWorld, blueMapService, plugin));
}
@Override
@ -127,7 +131,7 @@ public class BlueMapAPIImpl extends BlueMapAPI {
}
public Optional<BlueMapMap> getMapUncached(String id) {
var maps = plugin.getBlueMap().getMaps();
var maps = blueMapService.getMaps();
var map = maps.get(id);
if (map == null) return Optional.empty();
@ -135,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
@ -143,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());
}
}
@ -154,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,9 +26,11 @@ 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.lang.ref.WeakReference;
import java.nio.file.Path;
@ -39,17 +41,15 @@ import java.util.stream.Collectors;
public class BlueMapWorldImpl implements BlueMapWorld {
private final String id;
private final WeakReference<Plugin> plugin;
private final WeakReference<World> world;
private final WeakReference<BlueMapService> blueMapService;
private final WeakReference<Plugin> plugin;
public BlueMapWorldImpl(Plugin plugin, World world) {
public BlueMapWorldImpl(World world, BlueMapService blueMapService, @Nullable Plugin plugin) {
this.id = world.getId();
this.plugin = new WeakReference<>(plugin);
this.world = new WeakReference<>(world);
}
public World getWorld() {
return unpack(world);
this.blueMapService = new WeakReference<>(blueMapService);
this.plugin = new WeakReference<>(plugin);
}
@Override
@ -70,16 +70,40 @@ public class BlueMapWorldImpl implements BlueMapWorld {
@Override
public Collection<BlueMapMap> getMaps() {
Plugin plugin = unpack(this.plugin);
World world = unpack(this.world);
return plugin.getBlueMap().getMaps().values().stream()
return unpack(blueMapService).getMaps().values().stream()
.filter(map -> map.getWorld().equals(world))
.map(map -> new BlueMapMapImpl(plugin, map, this))
.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

View File

@ -25,36 +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.getBlueMap().getConfig().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 {
@ -64,31 +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) {
public synchronized void registerScript(String url) {
Logger.global.logDebug("Registering script from API: " + url);
plugin.getBlueMap().getWebFilesManager().getScripts().add(url);
blueMapService.getWebFilesManager().getScripts().add(url);
scheduleUpdateWebAppSettings();
}
@Override
public void registerStyle(String url) {
public synchronized void registerStyle(String url) {
Logger.global.logDebug("Registering style from API: " + url);
plugin.getBlueMap().getWebFilesManager().getStyles().add(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);
@ -102,11 +143,12 @@ 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,14 +24,12 @@
*/
package de.bluecolored.bluemap.common.config;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.BlueMapConfiguration;
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.datapack.DataPack;
import de.bluecolored.bluemap.core.resources.pack.datapack.DataPack;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Key;
import lombok.Builder;
@ -45,32 +43,43 @@ import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Stream;
@DebugDump
@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 ConfigManager configManager;
private final MinecraftVersion minecraftVersion;
private final CoreConfig coreConfig;
private final WebserverConfig webserverConfig;
private final WebappConfig webappConfig;
private final PluginConfig pluginConfig;
private final Map<String, MapConfig> mapConfigs;
private final Map<String, StorageConfig> storageConfigs;
private final Path resourcePacksFolder;
private final Path packsFolder;
private final @Nullable String minecraftVersion;
private final @Nullable Path modsFolder;
@Builder
private BlueMapConfigManager(
@NonNull MinecraftVersion minecraftVersion,
@NonNull Path configRoot,
@Nullable String minecraftVersion,
@Nullable Path defaultDataFolder,
@Nullable Path defaultWebroot,
@Nullable Collection<ServerWorld> autoConfigWorlds,
@Nullable Boolean usePluginConfig,
@Nullable Boolean useMetricsConfig,
@Nullable Path resourcePacksFolder,
@Nullable Path packsFolder,
@Nullable Path modsFolder
) throws ConfigurationException {
// set defaults
@ -79,10 +88,9 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
if (autoConfigWorlds == null) autoConfigWorlds = Collections.emptyList();
if (usePluginConfig == null) usePluginConfig = true;
if (useMetricsConfig == null) useMetricsConfig = true;
if (resourcePacksFolder == null) resourcePacksFolder = configRoot.resolve("resourcepacks");
if (packsFolder == null) packsFolder = configRoot.resolve("packs");
// load
this.minecraftVersion = minecraftVersion;
this.configManager = new ConfigManager(configRoot);
this.coreConfig = loadCoreConfig(defaultDataFolder, useMetricsConfig);
this.webappConfig = loadWebappConfig(defaultWebroot);
@ -90,21 +98,21 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
this.pluginConfig = usePluginConfig ? loadPluginConfig() : new PluginConfig();
this.storageConfigs = Collections.unmodifiableMap(loadStorageConfigs(webappConfig.getWebroot()));
this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs(autoConfigWorlds));
this.resourcePacksFolder = resourcePacksFolder;
this.packsFolder = packsFolder;
this.minecraftVersion = minecraftVersion;
this.modsFolder = modsFolder;
}
private CoreConfig loadCoreConfig(Path defaultDataFolder, boolean useMetricsConfig) throws ConfigurationException {
Path configFileRaw = Path.of("core");
Path configFile = configManager.findConfigPath(configFileRaw);
Path configFile = configManager.resolveConfigFile(CORE_CONFIG_NAME);
Path configFolder = configFile.getParent();
if (!Files.exists(configFile)) {
try {
FileHelper.createDirectories(configFolder);
Files.writeString(
configFolder.resolve("core.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/core.conf")
configFile,
configManager.loadConfigTemplate(CORE_CONFIG_NAME)
.setConditional("metrics", useMetricsConfig)
.setVariable("timestamp", LocalDateTime.now().withNano(0).toString())
.setVariable("version", BlueMap.VERSION)
@ -121,7 +129,7 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
}
}
return configManager.loadConfig(configFileRaw, CoreConfig.class);
return configManager.loadConfig(CORE_CONFIG_NAME, CoreConfig.class);
}
/**
@ -140,16 +148,15 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
}
private WebserverConfig loadWebserverConfig(Path defaultWebroot, Path dataRoot) throws ConfigurationException {
Path configFileRaw = Path.of("webserver");
Path configFile = configManager.findConfigPath(configFileRaw);
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")))
@ -161,20 +168,19 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
}
}
return configManager.loadConfig(configFileRaw, WebserverConfig.class);
return configManager.loadConfig(WEBSERVER_CONFIG_NAME, WebserverConfig.class);
}
private WebappConfig loadWebappConfig(Path defaultWebroot) throws ConfigurationException {
Path configFileRaw = Path.of("webapp");
Path configFile = configManager.findConfigPath(configFileRaw);
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
@ -184,20 +190,19 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
}
}
return configManager.loadConfig(configFileRaw, WebappConfig.class);
return configManager.loadConfig(WEBAPP_CONFIG_NAME, WebappConfig.class);
}
private PluginConfig loadPluginConfig() throws ConfigurationException {
Path configFileRaw = Path.of("plugin");
Path configFile = configManager.findConfigPath(configFileRaw);
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
);
@ -206,14 +211,13 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
}
}
return configManager.loadConfig(configFileRaw, PluginConfig.class);
return configManager.loadConfig(PLUGIN_CONFIG_NAME, PluginConfig.class);
}
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 {
@ -221,19 +225,19 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
if (autoConfigWorlds.isEmpty()) {
Path worldFolder = Path.of("world");
Files.writeString(
mapConfigFolder.resolve("overworld.conf"),
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"),
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"),
configManager.resolveConfigFile(MAPS_CONFIG_FOLDER_NAME + "/end"),
createEndMapTemplate("End", worldFolder,
DataPack.DIMENSION_THE_END, 0).build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
@ -265,22 +269,15 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
uniqueId = id + "_" + (++i);
mapIds.add(uniqueId);
Path configFile = mapConfigFolder.resolve(uniqueId + ".conf");
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().getFormatted()) {
case "minecraft:the_nether":
template = createNetherMapTemplate(name, worldFolder, dimension, i - 1);
break;
case "minecraft:the_end":
template = createEndMapTemplate(name, worldFolder, dimension, i - 1);
break;
default:
template = createOverworldMapTemplate(name, worldFolder, dimension, 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,
@ -300,8 +297,7 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
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" +
@ -309,7 +305,7 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
"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) {
@ -325,22 +321,21 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
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) {
@ -355,11 +350,10 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
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);
}
@ -378,7 +372,7 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
}
private ConfigTemplate createOverworldMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
return configManager.loadConfigTemplate(MAP_STORAGE_CONFIG_NAME)
.setVariable("name", name)
.setVariable("sorting", "" + index)
.setVariable("world", formatPath(worldFolder))
@ -392,7 +386,7 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
}
private ConfigTemplate createNetherMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
return configManager.loadConfigTemplate(MAP_STORAGE_CONFIG_NAME)
.setVariable("name", name)
.setVariable("sorting", "" + (100 + index))
.setVariable("world", formatPath(worldFolder))
@ -406,7 +400,7 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
}
private ConfigTemplate createEndMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
return configManager.loadConfigTemplate(MAP_STORAGE_CONFIG_NAME)
.setVariable("name", name)
.setVariable("sorting", "" + (200 + index))
.setVariable("world", formatPath(worldFolder))

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

@ -29,28 +29,24 @@ import de.bluecolored.bluemap.common.config.typeserializer.KeyTypeSerializer;
import de.bluecolored.bluemap.common.config.typeserializer.Vector2iTypeSerializer;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.util.Key;
import org.apache.commons.io.IOUtils;
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;
@ -58,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 {
@ -110,58 +142,17 @@ 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)

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,7 +26,6 @@ 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;
@ -38,7 +37,6 @@ import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.nio.file.Path;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
@Getter
public class MapConfig implements MapSettings {

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

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

@ -25,50 +25,36 @@
package de.bluecolored.bluemap.common.plugin;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.debug.DebugDump;
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.FileHelper;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluemap.core.util.WatchService;
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 {
public class MapUpdateService extends Thread {
private final BmMap map;
private final RenderManager renderManager;
private final WatchService watchService;
private final WatchService<Vector2i> watchService;
private volatile boolean closed;
private Timer delayTimer;
@DebugDump
private final Map<Vector2i, TimerTask> scheduledUpdates;
public RegionFileWatchService(RenderManager renderManager, BmMap map) throws IOException {
public MapUpdateService(RenderManager renderManager, BmMap map) throws IOException {
this.renderManager = renderManager;
this.map = map;
this.closed = false;
this.scheduledUpdates = new HashMap<>();
World world = map.getWorld();
if (!(world instanceof MCAWorld)) throw new UnsupportedOperationException("world-type is not supported");
Path folder = ((MCAWorld) world).getRegionFolder();
FileHelper.createDirectories(folder);
this.watchService = folder.getFileSystem().newWatchService();
folder.register(this.watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY);
Logger.global.logDebug("Created region-file watch-service for map '" + map.getId() + "' at '" + folder + "'.");
this.watchService = map.getWorld().createRegionWatchService();
}
@Override
@ -78,25 +64,9 @@ public class RegionFileWatchService extends Thread {
Logger.global.logDebug("Started watching map '" + map.getId() + "' for updates...");
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) {
while (!closed)
this.watchService.take().forEach(this::updateRegion);
} catch (WatchService.ClosedException ignore) {
} catch (InterruptedException iex) {
Thread.currentThread().interrupt();
} finally {
@ -108,37 +78,25 @@ public class RegionFileWatchService extends Thread {
}
}
private synchronized void updateRegion(String regionFileName) {
if (!regionFileName.endsWith(".mca")) return;
if (!regionFileName.startsWith("r.")) return;
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();
try {
String[] filenameParts = regionFileName.split("\\.");
if (filenameParts.length < 3) return;
task = new TimerTask() {
@Override
public void run() {
synchronized (MapUpdateService.this) {
WorldRegionRenderTask task = new WorldRegionRenderTask(map, regionPos);
scheduledUpdates.remove(regionPos);
renderManager.scheduleRenderTask(task);
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 5 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);
Logger.global.logDebug("Scheduled update for region-file: " + regionPos + " (Map: " + map.getId() + ")");
}
Logger.global.logDebug("Scheduled update for region-file: " + regionPos + " (Map: " + map.getId() + ")");
}
};
scheduledUpdates.put(regionPos, task);
delayTimer.schedule(task, 5000);
} catch (NumberFormatException ignore) {}
}
};
scheduledUpdates.put(regionPos, task);
delayTimer.schedule(task, 5000);
}
public void close() {
@ -149,7 +107,7 @@ public class RegionFileWatchService extends Thread {
try {
this.watchService.close();
} catch (IOException ex) {
} catch (Exception ex) {
Logger.global.logError("Exception while trying to close WatchService!", ex);
}
}

View File

@ -24,32 +24,34 @@
*/
package de.bluecolored.bluemap.common.plugin;
import de.bluecolored.bluemap.api.debug.DebugDump;
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.ServerEventListener;
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
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 de.bluecolored.bluemap.core.world.mca.MCAWorld;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.serialize.SerializationException;
@ -61,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;
@ -70,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";
@ -78,25 +81,23 @@ 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 Server serverInterface;
private BlueMapService blueMap;
private PluginState pluginState;
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;
@ -120,11 +121,17 @@ 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
BlueMapConfigManager configManager = BlueMapConfigManager.builder()
.minecraftVersion(serverInterface.getMinecraftVersion())
.configRoot(serverInterface.getConfigFolder())
.resourcePacksFolder(serverInterface.getConfigFolder().resolve("resourcepacks"))
.packsFolder(serverInterface.getConfigFolder().resolve("packs"))
.modsFolder(serverInterface.getModsFolder().orElse(null))
.useMetricsConfig(serverInterface.isMetricsEnabled() == Tristate.UNDEFINED)
.autoConfigWorlds(serverInterface.getLoadedServerWorlds())
@ -168,7 +175,7 @@ public class Plugin implements ServerEventListener {
BlueMapConfiguration configProvider = blueMap.getConfig();
if (configProvider instanceof BlueMapConfigManager) {
Logger.global.logWarning("Please check: " + ((BlueMapConfigManager) configProvider).getConfigManager().findConfigPath(Path.of("core")).toAbsolutePath().normalize());
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");
@ -185,10 +192,10 @@ public class Plugin implements ServerEventListener {
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 : configManager.getMapConfigs().entrySet()) {
@ -201,10 +208,10 @@ public class Plugin implements ServerEventListener {
mapRequestHandler = new MapRequestHandler(map, serverInterface, pluginConfig, Predicate.not(pluginState::isPlayerHidden));
} else {
Storage storage = blueMap.getOrLoadStorage(mapConfig.getStorage());
mapRequestHandler = new MapRequestHandler(id, storage);
mapRequestHandler = new MapRequestHandler(storage.map(id));
}
routingRequestHandler.register(
webRequestHandler.register(
"maps/" + Pattern.quote(id) + "/(.*)",
"$1",
new BlueMapResponseModifier(mapRequestHandler)
@ -224,7 +231,7 @@ public class Plugin implements ServerEventListener {
try {
webServer = new HttpServer(new LoggingRequestHandler(
routingRequestHandler,
webRequestHandler,
webserverConfig.getLog().getFormat(),
webLogger
));
@ -240,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);
}
}
@ -285,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();
@ -315,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();
}
};
@ -339,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
@ -359,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
@ -387,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();
@ -407,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();
@ -422,8 +437,11 @@ public class Plugin implements ServerEventListener {
Thread.currentThread().interrupt();
}
}
renderManager = null;
//save
save();
// stop webserver
if (webServer != null && !keepWebserver) {
try {
webServer.close();
@ -433,7 +451,7 @@ public class Plugin implements ServerEventListener {
webServer = null;
}
if (webLogger != null) {
if (webLogger != null && !keepWebserver) {
try {
webLogger.close();
} catch (Exception ex) {
@ -539,7 +557,7 @@ public class Plugin implements ServerEventListener {
Predicate.not(pluginState::isPlayerHidden)
);
try (
OutputStream out = map.getStorage().writeMeta(map.getId(), BmMap.META_FILE_PLAYERS);
OutputStream out = map.getStorage().players().write();
Writer writer = new OutputStreamWriter(out)
) {
writer.write(dataSupplier.get());
@ -553,16 +571,20 @@ public class Plugin implements ServerEventListener {
stopWatchingMap(map);
try {
RegionFileWatchService watcher = new RegionFileWatchService(renderManager, map);
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();
}
@ -616,42 +638,10 @@ public class Plugin implements ServerEventListener {
}
public @Nullable World getWorld(ServerWorld serverWorld) {
String id = MCAWorld.id(serverWorld.getWorldFolder(), serverWorld.getDimension());
String id = World.id(serverWorld.getWorldFolder(), serverWorld.getDimension());
return getBlueMap().getWorlds().get(id);
}
public Server getServerInterface() {
return serverInterface;
}
public BlueMapService getBlueMap() {
return blueMap;
}
public PluginState getPluginState() {
return pluginState;
}
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;
}
private void initFileWatcherTasks() {
var maps = blueMap.getMaps();
if (maps != null) {

View File

@ -30,12 +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 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 {
@ -96,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));
}
}
}

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,15 +48,19 @@ 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.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;
@ -66,8 +71,6 @@ 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;
@ -123,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)
@ -169,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")
@ -224,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);
@ -329,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;
}
@ -467,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());
@ -523,11 +618,20 @@ public class Commands<S> {
lines.put("chunk-has-lightdata", chunk.hasLightData());
lines.put("chunk-inhabited-time", chunk.getInhabitedTime());
lines.put("block-state", block.getBlockState());
lines.put("biome", block.getBiomeId());
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);
@ -540,7 +644,7 @@ public class Commands<S> {
final CommandSource source = commandSourceInterface.apply(context.getSource());
try {
Path file = plugin.getBlueMap().getConfig().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));
@ -666,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
@ -700,8 +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;
}
}
@ -718,8 +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;
}
@ -749,14 +855,9 @@ 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)"));
}
@ -869,7 +970,7 @@ public class Commands<S> {
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0World: ",
TextColor.DARK_GRAY, map.getWorld().getId()));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0Last Update: ",
TextColor.DARK_GRAY, helper.formatTime(map.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!"));
@ -886,8 +987,13 @@ public class Commands<S> {
source.sendMessage(Text.of(TextColor.BLUE, "Storages loaded by BlueMap:"));
for (var entry : plugin.getBlueMap().getConfig().getStorageConfigs().entrySet()) {
String storageTypeKey = "?";
try {
storageTypeKey = entry.getValue().getStorageType().getKey().getFormatted();
} catch (ConfigurationException ignore) {} // should never happen
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, entry.getKey())
.setHoverText(Text.of(entry.getValue().getStorageType().name()))
.setHoverText(Text.of(storageTypeKey))
.setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap storages " + entry.getKey())
);
}
@ -910,7 +1016,7 @@ public class Commands<S> {
Collection<String> mapIds;
try {
mapIds = storage.collectMapIds();
mapIds = storage.mapIds().toList();
} catch (IOException ex) {
Logger.global.logError("Unexpected exception trying to load mapIds from storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to access this storage. Please check the console for more details..."));
@ -923,7 +1029,7 @@ public class Commands<S> {
} else {
for (String mapId : mapIds) {
BmMap map = plugin.getBlueMap().getMaps().get(mapId);
boolean isLoaded = map != null && map.getStorage().equals(storage);
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)"));
@ -941,9 +1047,9 @@ 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().getOrLoadStorage(storageId);
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..."));

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;
@ -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.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,33 +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 = map.getMapSettings().getRenderBoundariesCellFilter(regionGrid);
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.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
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();
// collect chunks
long changesSince = force ? 0 : map.getRenderState().getRenderTime(worldRegion);
Region region = map.getWorld().getRegion(worldRegion.getX(), worldRegion.getY());
Collection<Vector2i> chunks = new ArrayList<>(1024);
// 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);
// load chunk-hash array
int chunkMaxCount = chunksSize.getX() * chunksSize.getY();
try {
region.iterateAllChunks((ChunkConsumer.ListOnly) (x, z, timestamp) -> {
if (timestamp >= changesSince) chunks.add(new Vector2i(x, z));
});
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.logWarning("Failed to read region " + worldRegion + " from world " + map.getWorld().getName() + " (" + ex + ")");
Logger.global.logError("Failed to load chunks for region " + regionPos, ex);
cancel();
}
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid chunkGrid = map.getWorld().getChunkGrid();
Predicate<Vector2i> boundsTileFilter = map.getMapSettings().getRenderBoundariesCellFilter(tileGrid);
// 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();
for (Vector2i chunk : chunks) {
Vector2i tileMin = chunkGrid.getCellMin(chunk, tileGrid);
Vector2i tileMax = chunkGrid.getCellMax(chunk, tileGrid);
int tileIndex = tileIndex(x, z);
tileActions[tileIndex] = tileState.findActionAndNextState(
force.test(tileState) || checkChunksHaveChanges(tile),
checkTileBounds(tile)
);
for (int x = tileMin.getX(); x <= tileMax.getX(); x++) {
for (int z = tileMin.getY(); z <= tileMax.getY(); z++) {
tileSet.add(new Vector2l(x, z));
}
if (tileActions[tileIndex].action() == RENDER)
tileRenderCount++;
if (tileActions[tileIndex].action() == DELETE)
tileDeleteCount++;
}
// make sure chunk gets re-loaded from disk
map.getWorld().invalidateChunkCache(chunk.getX(), chunk.getY());
}
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 (tileRenderCount >= tileMaxCount * 0.75)
map.getWorld().preloadRegionChunks(regionPos.getX(), regionPos.getY());
if (tileRenderCount + tileDeleteCount == 0)
completed = true;
if (tiles.isEmpty()) complete();
else {
// preload chunks
map.getWorld().preloadRegionChunks(worldRegion.getX(), worldRegion.getY());
}
}
@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++;
}
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.hasLightData() && !map.getMapSettings().isIgnoreMissingLightData()) 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;
}
// 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

@ -24,11 +24,11 @@
*/
package de.bluecolored.bluemap.common.serverinterface;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.MinecraftVersion;
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;
@ -37,7 +37,7 @@ import java.util.Optional;
public interface Server {
@DebugDump
MinecraftVersion getMinecraftVersion();
@Nullable String getMinecraftVersion();
/**
* Returns the Folder containing the configurations for the plugin

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

View File

@ -24,18 +24,20 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.api.debug.DebugDump;
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.BlueMap;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
@DebugDump
@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

@ -24,40 +24,46 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.api.debug.DebugDump;
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;
@DebugDump
@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
@ -76,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)) {
@ -133,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);
@ -146,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

@ -24,16 +24,20 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.api.debug.DebugDump;
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;
@DebugDump
@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);
@ -47,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

@ -30,6 +30,7 @@ import de.bluecolored.bluemap.common.live.LivePlayersDataSupplier;
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;
@ -40,20 +41,20 @@ import java.util.function.Supplier;
public class MapRequestHandler extends RoutingRequestHandler {
public MapRequestHandler(BmMap map, Server serverInterface, PluginConfig pluginConfig, Predicate<UUID> playerFilter) {
this(map.getId(), map.getStorage(),
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(

View File

@ -24,43 +24,39 @@
*/
package de.bluecolored.bluemap.common.web;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.ContentTypeRegistry;
import de.bluecolored.bluemap.api.debug.DebugDump;
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;
@DebugDump
@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();
@ -77,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;
}
@ -133,69 +112,31 @@ public class MapStorageRequestHandler implements HttpRequestHandler {
return new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
return new HttpResponse(HttpStatusCode.NO_CONTENT);
}
private String calculateETag(String path, TileInfo tileInfo) {
return Long.toHexString(tileInfo.getSize()) + Integer.toHexString(path.hashCode()) + Long.toHexString(tileInfo.getLastModified());
return new HttpResponse(HttpStatusCode.NOT_FOUND);
}
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));
} else {
response.setData(new BufferedInputStream(data.decompress()));
}
}
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);
response.setData(data.decompress());
}
}

View File

@ -24,24 +24,28 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.api.debug.DebugDump;
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;
@DebugDump
@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) {
@ -79,37 +83,20 @@ public class RoutingRequestHandler implements HttpRequestHandler {
return new HttpResponse(HttpStatusCode.BAD_REQUEST);
}
@DebugDump
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,14 +24,15 @@
*/
package de.bluecolored.bluemap.common.web.http;
import de.bluecolored.bluemap.api.debug.DebugDump;
import lombok.Getter;
import lombok.Setter;
import java.io.IOException;
@DebugDump
public class HttpServer extends Server {
private final HttpRequestHandler requestHandler;
@Getter @Setter
private HttpRequestHandler requestHandler;
public HttpServer(HttpRequestHandler requestHandler) throws IOException {
this.requestHandler = requestHandler;
@ -40,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

@ -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.5.2"
"vite": "^4.5.3"
}
},
"node_modules/@babel/parser": {
@ -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"
}
},
@ -2179,9 +2179,9 @@
"dev": true
},
"node_modules/vite": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",
@ -2613,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": {}
},
@ -3820,9 +3820,9 @@
"dev": true
},
"vite": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"requires": {
"esbuild": "^0.18.10",

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.5.2"
"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

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

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

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

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

@ -154,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) {
@ -180,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();
@ -299,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
@ -523,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;
@ -606,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");
@ -627,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");
@ -726,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,
@ -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

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

View File

@ -33,6 +33,7 @@ attribute float sunlight;
attribute float blocklight;
varying vec3 vPosition;
varying vec3 vWorldPosition;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vColor;
@ -42,6 +43,8 @@ varying float vBlocklight;
void main() {
vPosition = position;
vec4 worldPos = modelMatrix * vec4(vPosition, 1);
vWorldPosition = worldPos.xyz;
vNormal = normal;
vUv = uv;
vColor = color;

View File

@ -24,6 +24,7 @@
*/
import { ShaderChunk } from 'three';
// language=GLSL
export const LOWRES_FRAGMENT_SHADER = `
${ShaderChunk.logdepthbuf_pars_fragment}
@ -51,6 +52,7 @@ uniform vec2 textureSize;
uniform float lod;
uniform float lodScale;
uniform vec3 voidColor;
uniform bool chunkBorders;
varying vec3 vPosition;
varying vec3 vWorldPosition;
@ -98,9 +100,10 @@ void main() {
float ao = 0.0;
float aoStrength = 0.0;
float distFac = smoothstep(200.0, 600.0, distance);
if(lod == 1.0) {
aoStrength = smoothstep(PI - 0.8, PI - 0.2, acos(-clamp(viewMatrix[1][2], 0.0, 1.0)));
aoStrength *= 1.0 - smoothstep(200.0, 600.0, distance);
aoStrength *= 1.0 - distFac;
if (aoStrength > 0.0) {
const float r = 3.0;
@ -123,6 +126,21 @@ void main() {
float light = mix(blockLight, 15.0, 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;
color.rgb = mix(mix(color.rgb, lineColour.rgb, float(isChunkBorder) * lineColour.a), color.rgb, distFac);
}
vec3 adjustedVoidColor = adjustColor(voidColor);
//where there's transparency, there is void that needs to be coloured
color.rgb = mix(adjustedVoidColor, color.rgb, color.a);

View File

@ -208,7 +208,8 @@ var CSS2DRenderer = function (events = null) {
for ( var i = 0, l = sorted.length; i < l; i ++ ) {
sorted[ i ].element.style.zIndex = zMax - i;
let o = sorted[ i ];
o.element.style.zIndex = o.disableDepthTest ? zMax + 1 : zMax - i;
}

View File

@ -33,7 +33,7 @@ export const VEC3_Z = new Vector3(0, 0, 1);
/**
* Converts a url-encoded image string to an actual image-element
* @param string {string}
* @returns {HTMLElement}
* @returns {HTMLImageElement}
*/
export const stringToImage = string => {
let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');

View File

@ -38,15 +38,16 @@ val lastVersion = if (lastTag.isEmpty()) "dev" else lastTag.substring(1) // remo
val commits = "git rev-list --count $lastTag..HEAD".runCommand()
println("Git hash: $gitHash" + if (clean) "" else " (dirty)")
group = "de.bluecolored.bluemap.core"
group = "de.bluecolored.bluemap"
version = lastVersion +
(if (commits == "0") "" else "-$commits") +
(if (clean) "" else "-dirty")
System.setProperty("bluemap.version", version.toString())
System.setProperty("bluemap.lastVersion", lastVersion)
println("Version: $version")
val javaTarget = 11
val javaTarget = 16
java {
sourceCompatibility = JavaVersion.toVersion(javaTarget)
targetCompatibility = JavaVersion.toVersion(javaTarget)
@ -54,34 +55,30 @@ java {
repositories {
mavenCentral()
maven {
setUrl("https://jitpack.io")
}
maven ("https://repo.bluecolored.de/releases")
}
@Suppress("GradlePackageUpdate")
dependencies {
api ("com.github.ben-manes.caffeine:caffeine:2.8.5")
api ("org.apache.commons:commons-lang3:3.6")
api ("commons-io:commons-io:2.5")
api ("com.github.ben-manes.caffeine:caffeine:3.1.8")
api ("org.spongepowered:configurate-hocon:4.1.2")
api ("org.spongepowered:configurate-gson:4.1.2")
api ("com.github.BlueMap-Minecraft:BlueNBT:v1.3.0")
api ("de.bluecolored.bluenbt:BlueNBT:2.3.0")
api ("org.apache.commons:commons-dbcp2:2.9.0")
api ("io.airlift:aircompressor:0.24")
api ("org.lz4:lz4-java:1.8.0")
api ("de.bluecolored.bluemap.api:BlueMapAPI")
api ("de.bluecolored.bluemap:BlueMapAPI")
compileOnly ("org.jetbrains:annotations:23.0.0")
compileOnly ("org.projectlombok:lombok:1.18.30")
compileOnly ("org.projectlombok:lombok:1.18.32")
annotationProcessor ("org.projectlombok:lombok:1.18.30")
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")
testCompileOnly ("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor ("org.projectlombok:lombok:1.18.30")
testCompileOnly ("org.projectlombok:lombok:1.18.32")
testAnnotationProcessor ("org.projectlombok:lombok:1.18.32")
}
spotless {
@ -122,28 +119,11 @@ tasks.processResources {
}
}
//resource Extensions
val resourceIds: Array<String> = arrayOf(
"1_13", "1_15", "1_16", "1_18", "1_20_3"
)
tasks.register("zipResourceExtensions") {
resourceIds.forEach {
dependsOn("zipResourceExtensions$it")
}
}
resourceIds.forEach {
zipResourcesTask(it)
}
fun zipResourcesTask(resourceId: String) {
tasks.register ("zipResourceExtensions$resourceId", type = Zip::class) {
from(fileTree("src/main/resourceExtensions/mc$resourceId"))
archiveFileName.set("resourceExtensions.zip")
destinationDirectory.set(file("src/main/resources/de/bluecolored/bluemap/mc$resourceId/"))
outputs.upToDateWhen{ false }
}
tasks.register("zipResourceExtensions", type = Zip::class) {
from(fileTree("src/main/resourceExtensions"))
archiveFileName.set("resourceExtensions.zip")
destinationDirectory.set(file("src/main/resources/de/bluecolored/bluemap/"))
outputs.upToDateWhen{ false }
}
//always update the zip before build
@ -152,6 +132,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()
@ -159,6 +153,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
BlueMapCore/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

@ -1,181 +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.core;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.util.Lazy;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@DebugDump
public class MinecraftVersion implements Comparable<MinecraftVersion> {
private static final Pattern VERSION_REGEX = Pattern.compile("(?<major>\\d+)\\.(?<minor>\\d+)(?:\\.(?<patch>\\d+))?(?:-(?:pre|rc)\\d+)?");
public static final MinecraftVersion LATEST_SUPPORTED = new MinecraftVersion(1, 20, 3);
public static final MinecraftVersion EARLIEST_SUPPORTED = new MinecraftVersion(1, 13);
private final int major, minor, patch;
private final Lazy<MinecraftResource> resource;
public MinecraftVersion(int major, int minor) {
this(major, minor, 0);
}
public MinecraftVersion(int major, int minor, int patch) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.resource = new Lazy<>(this::findBestMatchingResource);
}
public String getVersionString() {
return major + "." + minor + "." + patch;
}
public MinecraftResource getResource() {
return this.resource.getValue();
}
public boolean isAtLeast(MinecraftVersion minVersion) {
return compareTo(minVersion) >= 0;
}
public boolean isAtMost(MinecraftVersion maxVersion) {
return compareTo(maxVersion) <= 0;
}
public boolean isBefore(MinecraftVersion minVersion) {
return compareTo(minVersion) < 0;
}
public boolean isAfter(MinecraftVersion minVersion) {
return compareTo(minVersion) > 0;
}
@Override
public int compareTo(MinecraftVersion other) {
int result;
result = Integer.compare(major, other.major);
if (result != 0) return result;
result = Integer.compare(minor, other.minor);
if (result != 0) return result;
result = Integer.compare(patch, other.patch);
return result;
}
public boolean majorEquals(MinecraftVersion that) {
return major == that.major;
}
public boolean minorEquals(MinecraftVersion that) {
return major == that.major && minor == that.minor;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MinecraftVersion that = (MinecraftVersion) o;
return major == that.major && minor == that.minor && patch == that.patch;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch);
}
private MinecraftResource findBestMatchingResource() {
MinecraftResource[] resources = MinecraftResource.values();
Arrays.sort(resources, Comparator.comparing(MinecraftResource::getVersion).reversed());
for (MinecraftResource resource : resources){
if (isAtLeast(resource.version)) return resource;
}
return resources[resources.length - 1];
}
public static MinecraftVersion of(String versionString) {
Matcher matcher = VERSION_REGEX.matcher(versionString);
if (!matcher.matches()) throw new IllegalArgumentException("Not a valid version string!");
int major = Integer.parseInt(matcher.group("major"));
int minor = Integer.parseInt(matcher.group("minor"));
int patch = 0;
String patchString = matcher.group("patch");
if (patchString != null) patch = Integer.parseInt(patchString);
return new MinecraftVersion(major, minor, patch);
}
@DebugDump
public enum MinecraftResource {
MC_1_13 (new MinecraftVersion(1, 13), "mc1_13", "https://piston-data.mojang.com/v1/objects/30bfe37a8db404db11c7edf02cb5165817afb4d9/client.jar"),
MC_1_14 (new MinecraftVersion(1, 14), "mc1_13", "https://piston-data.mojang.com/v1/objects/8c325a0c5bd674dd747d6ebaa4c791fd363ad8a9/client.jar"),
MC_1_15 (new MinecraftVersion(1, 15), "mc1_15", "https://piston-data.mojang.com/v1/objects/e3f78cd16f9eb9a52307ed96ebec64241cc5b32d/client.jar"),
MC_1_16 (new MinecraftVersion(1, 16), "mc1_16", "https://piston-data.mojang.com/v1/objects/228fdf45541c4c2fe8aec4f20e880cb8fcd46621/client.jar"),
MC_1_16_2 (new MinecraftVersion(1, 16, 2), "mc1_16", "https://piston-data.mojang.com/v1/objects/653e97a2d1d76f87653f02242d243cdee48a5144/client.jar"),
MC_1_17 (new MinecraftVersion(1, 17), "mc1_16", "https://piston-data.mojang.com/v1/objects/1cf89c77ed5e72401b869f66410934804f3d6f52/client.jar"),
MC_1_18 (new MinecraftVersion(1, 18), "mc1_18", "https://piston-data.mojang.com/v1/objects/020aa79e63a7aab5d6f30e5ec7a6c08baee6b64c/client.jar"),
MC_1_19 (new MinecraftVersion(1, 19), "mc1_18", "https://piston-data.mojang.com/v1/objects/a45634ab061beb8c878ccbe4a59c3315f9c0266f/client.jar"),
MC_1_19_4 (new MinecraftVersion(1, 19, 4), "mc1_18", "https://piston-data.mojang.com/v1/objects/958928a560c9167687bea0cefeb7375da1e552a8/client.jar"),
MC_1_20 (new MinecraftVersion(1, 20), "mc1_18", "https://piston-data.mojang.com/v1/objects/e575a48efda46cf88111ba05b624ef90c520eef1/client.jar"),
MC_1_20_3 (new MinecraftVersion(1, 20, 3), "mc1_20_3", "https://piston-data.mojang.com/v1/objects/b178a327a96f2cf1c9f98a45e5588d654a3e4369/client.jar");
private final MinecraftVersion version;
private final String resourcePrefix;
private final String clientUrl;
MinecraftResource(MinecraftVersion version, String resourcePrefix, String clientUrl) {
this.version = version;
this.resourcePrefix = resourcePrefix;
this.clientUrl = clientUrl;
}
public MinecraftVersion getVersion() {
return version;
}
public String getResourcePrefix() {
return resourcePrefix;
}
public String getClientUrl() {
return clientUrl;
}
}
}

View File

@ -1,277 +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.core.debug;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.BlueMap;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.serialize.SerializationException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
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 {
GsonConfigurationLoader loader = GsonConfigurationLoader.builder()
.path(file)
.build();
ConfigurationNode node = loader.createNode();
collectSystemInfo(node.node("system-info"));
Set<Object> alreadyDumped = Collections.newSetFromMap(new WeakHashMap<>());
try {
ConfigurationNode threadDump = node.node("threads");
for (Thread thread : Thread.getAllStackTraces().keySet()) {
dumpInstance(thread, loader.defaultOptions(), threadDump.appendListNode(), alreadyDumped);
}
} catch (SecurityException ex){
node.node("threads").set(ex.toString());
}
ConfigurationNode dump = node.node("dump");
for (Object instance : instances) {
Class<?> type = instance.getClass();
ConfigurationNode instanceDump = dump.node(type.getName()).appendListNode();
dumpInstance(instance, loader.defaultOptions(), instanceDump, alreadyDumped);
}
loader.save(node);
}
private void dumpInstance(Object instance, ConfigurationOptions options, ConfigurationNode node, Set<Object> alreadyDumped) throws SerializationException {
try {
if (instance == null){
node.raw(null);
return;
}
Class<?> type = instance.getClass();
if (!alreadyDumped.add(instance)) {
node.set("<<" + instance + ">>");
return;
}
if (instance instanceof Map) {
int count = 0;
Map<?, ?> map = (Map<?, ?>) instance;
if (map.isEmpty()){
node.set(map.toString());
return;
}
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (++count > 100) {
node.appendListNode().set("<<" + (map.size() - 100) + " more elements>>");
break;
}
ConfigurationNode entryNode = node.appendListNode();
dumpInstance(entry.getKey(), options, entryNode.node("key"), alreadyDumped);
dumpInstance(entry.getValue(), options, entryNode.node("value"), alreadyDumped);
}
return;
}
if (instance instanceof Collection) {
if (((Collection<?>) instance).isEmpty()){
node.set(instance.toString());
return;
}
int count = 0;
for (Object entry : (Collection<?>) instance) {
if (++count > 100) {
node.appendListNode().set("<<" + (((Collection<?>) instance).size() - 100) + " more elements>>");
break;
}
dumpInstance(entry, options, node.appendListNode(), alreadyDumped);
}
return;
}
if (instance instanceof Object[]) {
if (((Object[]) instance).length == 0){
node.set(instance.toString());
return;
}
int count = 0;
for (Object entry : (Object[]) instance) {
if (++count > 100) {
node.appendListNode().set("<<" + (((Object[]) instance).length - 100) + " more elements>>");
break;
}
dumpInstance(entry, options, node.appendListNode(), alreadyDumped);
}
return;
}
if (instance instanceof Thread) {
Thread t = (Thread) instance;
node.node("name").set(t.getName());
node.node("state").set(t.getState().toString());
node.node("priority").set(t.getPriority());
node.node("alive").set(t.isAlive());
node.node("id").set(t.getId());
node.node("deamon").set(t.isDaemon());
node.node("interrupted").set(t.isInterrupted());
dumpInstance(t.getStackTrace(), options, node.node("stackTrace"), alreadyDumped);
return;
}
boolean foundSomething = dumpAnnotatedInstance(type, instance, options, node, alreadyDumped);
if (!foundSomething) {
node.set(instance.toString());
}
} catch (Exception ex) {
StringWriter stringWriter = new StringWriter();
ex.printStackTrace(new PrintWriter(stringWriter));
node.set("Error: " + ex + " >> " + stringWriter);
}
}
private boolean dumpAnnotatedInstance(Class<?> type, Object instance, ConfigurationOptions options, ConfigurationNode node, Set<Object> alreadyDumped) throws Exception {
boolean foundSomething = false;
boolean allFields = type.isAnnotationPresent(DebugDump.class);
for (Field field : type.getDeclaredFields()) {
DebugDump dd = field.getAnnotation(DebugDump.class);
if (dd == null) {
if (!allFields) continue;
if (Modifier.isStatic(field.getModifiers())) continue;
if (Modifier.isTransient(field.getModifiers())) continue;
}
foundSomething = true;
String key = "";
if (dd != null) key = dd.value();
if (key.isEmpty()) key = field.getName();
field.setAccessible(true);
if (options.acceptsType(field.getType())) {
node.node(key).set(field.get(instance));
} else {
dumpInstance(field.get(instance), options, node.node(key), alreadyDumped);
}
}
for (Method method : type.getDeclaredMethods()) {
DebugDump dd = method.getAnnotation(DebugDump.class);
if (dd == null) continue;
foundSomething = true;
String key = dd.value();
if (key.isEmpty()) key = method.toGenericString().replace(' ', '_');
if (options.acceptsType(method.getReturnType())) {
method.setAccessible(true);
node.node(key).set(method.invoke(instance));
} else {
method.setAccessible(true);
dumpInstance(method.invoke(instance), options, node.node(key), alreadyDumped);
}
}
for (Class<?> iface : type.getInterfaces()) {
foundSomething |= dumpAnnotatedInstance(iface, instance, options, node, alreadyDumped);
}
Class<?> typeSuperclass = type.getSuperclass();
if (typeSuperclass != null) {
foundSomething |= dumpAnnotatedInstance(typeSuperclass, instance, options, node, alreadyDumped);
}
return foundSomething;
}
private void collectSystemInfo(ConfigurationNode node) throws SerializationException {
node.node("bluemap-version").set(BlueMap.VERSION);
node.node("git-hash").set(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));
}
node.node("system-properties").set(propMap);
node.node("cores").set(Runtime.getRuntime().availableProcessors());
node.node("max-memory").set(Runtime.getRuntime().maxMemory());
node.node("total-memory").set(Runtime.getRuntime().totalMemory());
node.node("free-memory").set(Runtime.getRuntime().freeMemory());
node.node("timestamp").set(System.currentTimeMillis());
node.node("time").set(LocalDateTime.now().toString());
}
public static StateDumper global() {
return GLOBAL;
}
public synchronized void register(Object instance) {
GLOBAL.instances.add(instance);
}
public synchronized void unregister(Object instance) {
GLOBAL.instances.remove(instance);
}
}

View File

@ -24,13 +24,12 @@
*/
package de.bluecolored.bluemap.core.logger;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import de.bluecolored.bluemap.core.BlueMap;
import java.util.concurrent.TimeUnit;
public abstract class AbstractLogger extends Logger {
private static final Object DUMMY = new Object();
@ -40,7 +39,7 @@ public abstract class AbstractLogger extends Logger {
public AbstractLogger() {
noFloodCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.expireAfterWrite(1, TimeUnit.HOURS)
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
}

View File

@ -28,35 +28,35 @@ import com.flowpowered.math.vector.Vector2i;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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.core.logger.Logger;
import de.bluecolored.bluemap.core.map.hires.HiresModelManager;
import de.bluecolored.bluemap.core.map.lowres.LowresTileManager;
import de.bluecolored.bluemap.core.map.renderstate.MapChunkState;
import de.bluecolored.bluemap.core.map.renderstate.MapTileState;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.io.*;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
@DebugDump
@Getter
public class BmMap {
public static final String META_FILE_SETTINGS = "settings.json";
public static final String META_FILE_TEXTURES = "textures.json";
public static final String META_FILE_RENDER_STATE = ".rstate";
public static final String META_FILE_MARKERS = "live/markers.json";
public static final String META_FILE_PLAYERS = "live/players.json";
private static final Gson GSON = ResourcesGson.addAdapter(new GsonBuilder())
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.registerTypeAdapter(BmMap.class, new MapSettingsSerializer())
@ -65,24 +65,27 @@ public class BmMap {
private final String id;
private final String name;
private final World world;
private final Storage storage;
private final MapStorage storage;
private final MapSettings mapSettings;
private final ResourcePack resourcePack;
private final MapRenderState renderState;
private final TextureGallery textureGallery;
private final MapTileState mapTileState;
private final MapChunkState mapChunkState;
private final HiresModelManager hiresModelManager;
private final LowresTileManager lowresTileManager;
private final ConcurrentHashMap<String, MarkerSet> markerSets;
private Predicate<Vector2i> tileFilter;
@Setter private Predicate<Vector2i> tileFilter;
private long renderTimeSumNanos;
private long tilesRendered;
@Getter(AccessLevel.NONE) private long renderTimeSumNanos;
@Getter(AccessLevel.NONE) private long tilesRendered;
@Getter(AccessLevel.NONE) private long lastSaveTime;
public BmMap(String id, String name, World world, Storage storage, ResourcePack resourcePack, MapSettings settings) throws IOException {
public BmMap(String id, String name, World world, MapStorage storage, ResourcePack resourcePack, MapSettings settings) throws IOException, InterruptedException {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.world = Objects.requireNonNull(world);
@ -90,15 +93,20 @@ public class BmMap {
this.resourcePack = Objects.requireNonNull(resourcePack);
this.mapSettings = Objects.requireNonNull(settings);
this.renderState = new MapRenderState();
loadRenderState();
Logger.global.logDebug("Loading render-state for map '" + id + "'");
this.mapTileState = new MapTileState(storage.tileState());
this.mapTileState.load();
this.mapChunkState = new MapChunkState(storage.chunkState());
if (Thread.interrupted()) throw new InterruptedException();
Logger.global.logDebug("Loading textures for map '" + id + "'");
this.textureGallery = loadTextureGallery();
this.textureGallery.put(resourcePack);
saveTextureGallery();
this.hiresModelManager = new HiresModelManager(
storage.tileStorage(id, 0),
storage.hiresTiles(),
this.resourcePack,
this.textureGallery,
settings,
@ -106,7 +114,7 @@ public class BmMap {
);
this.lowresTileManager = new LowresTileManager(
storage.mapStorage(id),
storage,
new Grid(settings.getLowresTileSize()),
settings.getLodCount(),
settings.getLodFactor()
@ -118,6 +126,7 @@ public class BmMap {
this.renderTimeSumNanos = 0;
this.tilesRendered = 0;
this.lastSaveTime = -1;
saveMapSettings();
}
@ -136,56 +145,51 @@ public class BmMap {
tilesRendered ++;
}
public void unrenderTile(Vector2i tile) {
hiresModelManager.unrender(tile, lowresTileManager);
}
public synchronized boolean save(long minTimeSinceLastSave) {
long now = System.currentTimeMillis();
if (now - lastSaveTime < minTimeSinceLastSave)
return false;
save();
return true;
}
public synchronized void save() {
lowresTileManager.save();
saveRenderState();
mapTileState.save();
mapChunkState.save();
saveMarkerState();
savePlayerState();
saveMapSettings();
// only save texture gallery if not present in storage
try {
if (storage.readMetaInfo(id, META_FILE_TEXTURES).isEmpty())
if (!storage.textures().exists())
saveTextureGallery();
} catch (IOException e) {
Logger.global.logError("Failed to read texture gallery", e);
Logger.global.logError("Failed to read texture gallery for map '" + getId() + "'!", e);
}
}
private void loadRenderState() throws IOException {
Optional<InputStream> rstateData = storage.readMeta(id, META_FILE_RENDER_STATE);
if (rstateData.isPresent()) {
try (InputStream in = rstateData.get()){
this.renderState.load(in);
} catch (IOException ex) {
Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex);
}
}
}
public synchronized void saveRenderState() {
try (OutputStream out = storage.writeMeta(id, META_FILE_RENDER_STATE)) {
this.renderState.save(out);
} catch (IOException ex){
Logger.global.logError("Failed to save render-state for map: '" + this.id + "'!", ex);
}
lastSaveTime = System.currentTimeMillis();
}
private TextureGallery loadTextureGallery() throws IOException {
TextureGallery gallery = null;
Optional<InputStream> texturesData = storage.readMeta(id, META_FILE_TEXTURES);
if (texturesData.isPresent()) {
try (InputStream in = texturesData.get()){
gallery = TextureGallery.readTexturesFile(in);
} catch (IOException ex) {
Logger.global.logError("Failed to load textures for map '" + getId() + "'!", ex);
}
try (CompressedInputStream in = storage.textures().read()){
if (in != null)
return TextureGallery.readTexturesFile(in.decompress());
} catch (IOException ex) {
Logger.global.logError("Failed to load textures for map '" + getId() + "'!", ex);
}
return gallery != null ? gallery : new TextureGallery();
return new TextureGallery();
}
private void saveTextureGallery() {
try (OutputStream out = storage.writeMeta(id, META_FILE_TEXTURES)) {
try (OutputStream out = storage.textures().write()) {
this.textureGallery.writeTexturesFile(out);
} catch (IOException ex) {
Logger.global.logError("Failed to save textures for map '" + getId() + "'!", ex);
@ -199,7 +203,7 @@ public class BmMap {
private void saveMapSettings() {
try (
OutputStream out = storage.writeMeta(id, META_FILE_SETTINGS);
OutputStream out = storage.settings().write();
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)
) {
GSON.toJson(this, writer);
@ -210,7 +214,7 @@ public class BmMap {
public synchronized void saveMarkerState() {
try (
OutputStream out = storage.writeMeta(id, META_FILE_MARKERS);
OutputStream out = storage.markers().write();
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)
) {
MarkerGson.INSTANCE.toJson(this.markerSets, writer);
@ -220,59 +224,13 @@ public class BmMap {
}
public synchronized void savePlayerState() {
try (
OutputStream out = storage.writeMeta(id, META_FILE_PLAYERS)
) {
try (OutputStream out = storage.players().write()) {
out.write("{}".getBytes(StandardCharsets.UTF_8));
} catch (Exception ex) {
Logger.global.logError("Failed to save markers for map '" + getId() + "'!", ex);
}
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public World getWorld() {
return world;
}
public Storage getStorage() {
return storage;
}
public MapSettings getMapSettings() {
return mapSettings;
}
public MapRenderState getRenderState() {
return renderState;
}
public HiresModelManager getHiresModelManager() {
return hiresModelManager;
}
public LowresTileManager getLowresTileManager() {
return lowresTileManager;
}
public Map<String, MarkerSet> getMarkerSets() {
return markerSets;
}
public Predicate<Vector2i> getTileFilter() {
return tileFilter;
}
public void setTileFilter(Predicate<Vector2i> tileFilter) {
this.tileFilter = tileFilter;
}
public long getAverageNanosPerTile() {
return renderTimeSumNanos / tilesRendered;
}
@ -284,12 +242,8 @@ public class BmMap {
@Override
public boolean equals(Object obj) {
if (obj instanceof BmMap) {
BmMap that = (BmMap) obj;
if (obj instanceof BmMap that)
return this.id.equals(that.id);
}
return false;
}

View File

@ -1,119 +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.core.map;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.debug.DebugDump;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@DebugDump
public class MapRenderState {
private final Map<Vector2i, Long> regionRenderTimes;
private transient long latestRenderTime = -1;
public MapRenderState() {
regionRenderTimes = new HashMap<>();
}
public synchronized void setRenderTime(Vector2i regionPos, long renderTime) {
regionRenderTimes.put(regionPos, renderTime);
if (latestRenderTime != -1) {
if (renderTime > latestRenderTime)
latestRenderTime = renderTime;
else
latestRenderTime = -1;
}
}
public synchronized long getRenderTime(Vector2i regionPos) {
Long renderTime = regionRenderTimes.get(regionPos);
if (renderTime == null) return -1;
else return renderTime;
}
public long getLatestRenderTime() {
if (latestRenderTime == -1) {
synchronized (this) {
latestRenderTime = regionRenderTimes.values().stream()
.mapToLong(Long::longValue)
.max()
.orElse(-1);
}
}
return latestRenderTime;
}
public synchronized void reset() {
regionRenderTimes.clear();
}
public synchronized void save(OutputStream out) throws IOException {
try (
DataOutputStream dOut = new DataOutputStream(new GZIPOutputStream(out))
) {
dOut.writeInt(regionRenderTimes.size());
for (Map.Entry<Vector2i, Long> entry : regionRenderTimes.entrySet()) {
Vector2i regionPos = entry.getKey();
long renderTime = entry.getValue();
dOut.writeInt(regionPos.getX());
dOut.writeInt(regionPos.getY());
dOut.writeLong(renderTime);
}
dOut.flush();
}
}
public synchronized void load(InputStream in) throws IOException {
regionRenderTimes.clear();
try (
DataInputStream dIn = new DataInputStream(new GZIPInputStream(in))
) {
int size = dIn.readInt();
for (int i = 0; i < size; i++) {
Vector2i regionPos = new Vector2i(
dIn.readInt(),
dIn.readInt()
);
long renderTime = dIn.readLong();
regionRenderTimes.put(regionPos, renderTime);
}
} catch (EOFException ignore){} // ignoring a sudden end of stream, since it is save to only read as many as we can
}
}

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