Compare commits
87 Commits
Author | SHA1 | Date |
---|---|---|
Antti Ellilä | 0f472a818d | |
Kyryl Andreiev | bfec68e414 | |
Lukas Rieger (Blue) | 7895222816 | |
Lukas Rieger (Blue) | cca1fbc826 | |
Lukas Rieger (Blue) | eeb01cba1a | |
Lukas Rieger (Blue) | 8f88dd7fd9 | |
Lukas Rieger (Blue) | 33d40b888a | |
Lukas Rieger (Blue) | 6392b67744 | |
Lukas Rieger | fbac48cd86 | |
Lukas Rieger (Blue) | 4a38a7491b | |
Lukas Rieger (Blue) | 2dfd9feb21 | |
Lukas Rieger (Blue) | 75b562eeb1 | |
Lukas Rieger (Blue) | 474c5e27c4 | |
Lukas Rieger (Blue) | d43c7c474f | |
Lukas Rieger (Blue) | 4b8245d19d | |
Lukas Rieger (Blue) | b5e8bf42ae | |
Lukas Rieger (Blue) | 7afcbeefd7 | |
Lukas Rieger (Blue) | 0cc0247930 | |
Lukas Rieger (Blue) | e04e46fa5f | |
Lukas Rieger (Blue) | b1c75aa44a | |
Lukas Rieger (Blue) | 02d9fc1405 | |
Lukas Rieger (Blue) | 52d1e59108 | |
Lukas Rieger (Blue) | 51185f5884 | |
Lukas Rieger (Blue) | 6ad50a89cb | |
Lukas Rieger (Blue) | 01b1ac513c | |
TechnicJelle | fc8377764c | |
Lukas Rieger (Blue) | a594a4bed3 | |
Lukas Rieger (Blue) | 2c2d2f9227 | |
Lukas Rieger (Blue) | 8455b50fc3 | |
Lukas Rieger (Blue) | 3db6833fc6 | |
Lukas Rieger (Blue) | 20aa0a72f5 | |
Lukas Rieger (Blue) | 3faf2f0135 | |
Lukas Rieger (Blue) | d77d90c658 | |
Lukas Rieger (Blue) | d7dd8931a5 | |
Lukas Rieger (Blue) | ce25eb52e3 | |
Lukas Rieger (Blue) | 93d8876b20 | |
Lukas Rieger (Blue) | 3cd3f1d032 | |
Lukas Rieger (Blue) | 0b111463be | |
Lukas Rieger (Blue) | b330c5d168 | |
Lukas Rieger (Blue) | 2029fe0a87 | |
Lukas Rieger (Blue) | 2777846cf8 | |
Lukas Rieger (Blue) | 1b26803527 | |
Lukas Rieger | 7cdc8213fa | |
Salzian | fe5c1fa785 | |
Lukas Rieger (Blue) | 909642d4c3 | |
Lukas Rieger (Blue) | 05bbd2b481 | |
Lukas Rieger (Blue) | 36c1d3f7ac | |
TyBraniff | a311fc1cef | |
Lukas Rieger (Blue) | 81fe41fd2b | |
Lukas Rieger (Blue) | a6402850c9 | |
Lukas Rieger (Blue) | 37dd18190b | |
Lukas Rieger (Blue) | fa966c4363 | |
Lukas Rieger (Blue) | 240ca6c00e | |
Lukas Rieger (Blue) | f18f7a9a16 | |
Lukas Rieger (Blue) | f66437ac83 | |
Lukas Rieger (Blue) | fdf242acdf | |
Lukas Rieger (Blue) | 7e7b1e4f53 | |
Lukas Rieger (Blue) | f097517320 | |
Lukas Rieger (Blue) | ee3ab6ff9a | |
Lukas Rieger (Blue) | 498a4f3190 | |
Lukas Rieger (Blue) | 757979b7b4 | |
Lukas Rieger (Blue) | 6e8247ae3a | |
Gerber Lóránt Viktor | a847e247e5 | |
Lukas Rieger (Blue) | 2689cd10e0 | |
Lukas Rieger (Blue) | b60b14372f | |
Gerber Lóránt Viktor | 10fb88df4b | |
Lukas Rieger (Blue) | 9fca7b9361 | |
Lukas Rieger (Blue) | a0e9180360 | |
Lukas Rieger (Blue) | e9e7042aed | |
Lukas Rieger (Blue) | b27aedc4c2 | |
Lukas Rieger (Blue) | 0613037093 | |
Lukas Rieger (Blue) | aecbd23ba7 | |
Lukas Rieger (Blue) | 2899646adc | |
Lukas Rieger (Blue) | c9a8c83d6e | |
Lukas Rieger (Blue) | ceb31b68eb | |
Lukas Rieger (Blue) | b625af695c | |
Lukas Rieger (Blue) | 908789a815 | |
Nikita | c0c946d154 | |
Lukas Rieger (Blue) | b02b91d3bb | |
Antti Ellilä | b437684dbb | |
Lukas Rieger (Blue) | 5bb7a77fb9 | |
Lukas Rieger (Blue) | 0d36a0f70b | |
Lukas Rieger (Blue) | 35c236e9ce | |
Lukas Rieger (Blue) | 79ea7baba7 | |
Lukas Rieger (Blue) | 122ba83ebb | |
Lukas Rieger (Blue) | d1aba560da | |
Lukas Rieger (Blue) | 9e8dc8e5a8 |
|
@ -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
|
||||
|
|
|
@ -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(", "));
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "translation-checker",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node ."
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
@ -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
|
|
@ -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" "$@"
|
|
@ -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
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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 {}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
) {}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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..."));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) + "%)");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {};
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,6 +83,9 @@
|
|||
light: "Light"
|
||||
contrast: "Contrast"
|
||||
}
|
||||
chunkBorders: {
|
||||
button: "Show chunk borders"
|
||||
}
|
||||
debug: {
|
||||
button: "Debug"
|
||||
}
|
||||
|
|
|
@ -83,6 +83,9 @@
|
|||
light: "Vaalea"
|
||||
contrast: "Kontrasti"
|
||||
}
|
||||
chunkBorders: {
|
||||
button: "Näytä lohkojen rajat"
|
||||
}
|
||||
debug: {
|
||||
button: "Debug"
|
||||
}
|
||||
|
|
|
@ -83,6 +83,9 @@
|
|||
light: "Licht"
|
||||
contrast: "Contrast"
|
||||
}
|
||||
chunkBorders: {
|
||||
button: "Laat chunk grenzen zien"
|
||||
}
|
||||
debug: {
|
||||
button: "Debug"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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.
|
@ -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
|
|
@ -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" "$@"
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue