Skip to content

Commit 55e456c

Browse files
authored
Merge branch 'BlueMap-Minecraft:master' into feat/entity
2 parents 912370b + 11e4cd1 commit 55e456c

File tree

161 files changed

+4467
-1132
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

161 files changed

+4467
-1132
lines changed

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: Suggestion or Question
4+
url: https://bluecolo.red/map-discord
5+
about: You have a suggestion, a question or a problem that is not a bug? Please use our discord-server for this! :)

.github/workflows/translation-checker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Check translations
33
on:
44
push:
55
paths:
6-
- "BlueMapCommon/webapp/public/lang/**"
6+
- "common/webapp/public/lang/**"
77
- ".github/translation-checker/**"
88

99
permissions:

buildSrc/src/main/kotlin/curseforge.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import com.matthewprenger.cursegradle.CurseProject
2-
import gradle.kotlin.dsl.accessors._0d85c8abe599c3884bce56e72c18751a.curseforge
2+
import gradle.kotlin.dsl.accessors._30aa10cce2eda716151152e22de91a4c.curseforge
33
import org.gradle.api.Action
44
import org.gradle.api.Project
55
import org.gradle.kotlin.dsl.closureOf
@@ -14,4 +14,4 @@ fun Project.curseforgeBlueMap (configuration: Action<CurseProject>) {
1414

1515
configuration.execute(this)
1616
})
17-
}
17+
}

common/src/main/java/de/bluecolored/bluemap/common/BlueMapService.java

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,11 @@
2424
*/
2525
package de.bluecolored.bluemap.common;
2626

27-
import com.google.gson.FieldNamingPolicy;
28-
import com.google.gson.Gson;
29-
import com.google.gson.GsonBuilder;
30-
import com.google.gson.JsonParseException;
31-
import com.google.gson.reflect.TypeToken;
32-
import de.bluecolored.bluemap.api.gson.MarkerGson;
33-
import de.bluecolored.bluemap.api.markers.MarkerSet;
3427
import de.bluecolored.bluemap.common.config.ConfigurationException;
3528
import de.bluecolored.bluemap.common.config.MapConfig;
3629
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
37-
import de.bluecolored.bluemap.common.plugin.Plugin;
3830
import de.bluecolored.bluemap.common.debug.StateDumper;
31+
import de.bluecolored.bluemap.common.plugin.Plugin;
3932
import de.bluecolored.bluemap.core.logger.Logger;
4033
import de.bluecolored.bluemap.core.map.BmMap;
4134
import de.bluecolored.bluemap.core.resources.MinecraftVersion;
@@ -48,14 +41,9 @@
4841
import de.bluecolored.bluemap.core.world.World;
4942
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
5043
import org.jetbrains.annotations.Nullable;
51-
import org.spongepowered.configurate.ConfigurateException;
52-
import org.spongepowered.configurate.ConfigurationNode;
53-
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
54-
import org.spongepowered.configurate.loader.HeaderMode;
5544

5645
import java.io.Closeable;
5746
import java.io.IOException;
58-
import java.lang.reflect.Type;
5947
import java.net.URL;
6048
import java.nio.file.Files;
6149
import java.nio.file.Path;
@@ -165,11 +153,7 @@ public synchronized Map<String, BmMap> getOrLoadMaps(Predicate<String> filter) t
165153
try {
166154
loadMap(entry.getKey(), entry.getValue());
167155
} catch (ConfigurationException ex) {
168-
Logger.global.logWarning(ex.getFormattedExplanation());
169-
Throwable cause = ex.getRootCause();
170-
if (cause != null) {
171-
Logger.global.logError("Detailed error:", ex);
172-
}
156+
ex.printLog(Logger.global);
173157
}
174158
}
175159
return Collections.unmodifiableMap(maps);
@@ -249,27 +233,9 @@ private synchronized void loadMap(String id, MapConfig mapConfig) throws Configu
249233
);
250234
maps.put(id, map);
251235

252-
// load marker-config by converting it first from hocon to json and then loading it with MarkerGson
253-
ConfigurationNode markerSetNode = mapConfig.getMarkerSets();
254-
if (markerSetNode != null && !markerSetNode.empty()) {
255-
String markerJson = GsonConfigurationLoader.builder()
256-
.headerMode(HeaderMode.NONE)
257-
.lenient(false)
258-
.indent(0)
259-
.buildAndSaveString(markerSetNode);
260-
Gson gson = MarkerGson.addAdapters(new GsonBuilder())
261-
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
262-
.create();
263-
Type markerSetType = new TypeToken<Map<String, MarkerSet>>() {}.getType();
264-
Map<String, MarkerSet> markerSets = gson.fromJson(markerJson, markerSetType);
265-
map.getMarkerSets().putAll(markerSets);
266-
}
236+
// load markers
237+
map.getMarkerSets().putAll(mapConfig.parseMarkerSets());
267238

268-
} catch (ConfigurateException | JsonParseException ex) {
269-
throw new ConfigurationException(
270-
"Failed to create the markers for map '" + id + "'!\n" +
271-
"Make sure your marker-configuration for this map is valid.",
272-
ex);
273239
} catch (IOException | ConfigurationException ex) {
274240
throw new ConfigurationException("Failed to load map '" + id + "'!", ex);
275241
}

core/src/main/java/de/bluecolored/bluemap/core/resources/pack/resourcepack/ResourcePackExtensionType.java renamed to common/src/main/java/de/bluecolored/bluemap/common/addons/Addon.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
* THE SOFTWARE.
2424
*/
25-
package de.bluecolored.bluemap.core.resources.pack.resourcepack;
25+
package de.bluecolored.bluemap.common.addons;
2626

27-
import de.bluecolored.bluemap.core.util.Keyed;
28-
import de.bluecolored.bluemap.core.util.Registry;
27+
import lombok.Getter;
28+
import lombok.RequiredArgsConstructor;
2929

30-
public interface ResourcePackExtensionType<T extends ResourcePackExtension> extends Keyed {
30+
import java.nio.file.Path;
3131

32-
Registry<ResourcePackExtensionType<?>> REGISTRY = new Registry<>();
32+
@RequiredArgsConstructor
33+
@Getter
34+
public class Addon {
3335

34-
T create();
36+
private final AddonInfo addonInfo;
37+
private final Path jarFile;
3538

3639
}

common/src/main/java/de/bluecolored/bluemap/common/addons/AddonInfo.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,59 @@
2424
*/
2525
package de.bluecolored.bluemap.common.addons;
2626

27+
import com.google.gson.FieldNamingPolicy;
28+
import com.google.gson.Gson;
29+
import com.google.gson.GsonBuilder;
30+
import de.bluecolored.bluemap.common.config.ConfigurationException;
2731
import lombok.Getter;
32+
import org.jetbrains.annotations.Nullable;
2833

34+
import java.io.IOException;
35+
import java.io.Reader;
36+
import java.nio.charset.StandardCharsets;
37+
import java.nio.file.FileSystem;
38+
import java.nio.file.FileSystems;
39+
import java.nio.file.Files;
40+
import java.nio.file.Path;
41+
import java.util.Set;
42+
43+
@SuppressWarnings({"FieldMayBeFinal", "unused"})
2944
@Getter
3045
public class AddonInfo {
3146
public static final String ADDON_INFO_FILE = "bluemap.addon.json";
3247

48+
private static final Gson GSON = new GsonBuilder()
49+
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
50+
.create();
51+
3352
private String id;
3453
private String entrypoint;
54+
private Set<String> dependencies = Set.of();
55+
private Set<String> softDependencies = Set.of();
56+
57+
public static @Nullable AddonInfo load(Path addonJarFile) throws ConfigurationException {
58+
try (FileSystem fileSystem = FileSystems.newFileSystem(addonJarFile, (ClassLoader) null)) {
59+
for (Path root : fileSystem.getRootDirectories()) {
60+
Path addonInfoFile = root.resolve(ADDON_INFO_FILE);
61+
if (!Files.exists(addonInfoFile)) continue;
62+
63+
try (Reader reader = Files.newBufferedReader(addonInfoFile, StandardCharsets.UTF_8)) {
64+
AddonInfo addonInfo = GSON.fromJson(reader, AddonInfo.class);
65+
66+
if (addonInfo.getId() == null)
67+
throw new ConfigurationException("'id' is missing");
68+
69+
if (addonInfo.getEntrypoint() == null)
70+
throw new ConfigurationException("'entrypoint' is missing");
71+
72+
return addonInfo;
73+
}
74+
}
75+
} catch (IOException e) {
76+
throw new ConfigurationException("There was an exception trying to access the file.", e);
77+
}
78+
79+
return null;
80+
}
3581

3682
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
* This file is part of BlueMap, licensed under the MIT License (MIT).
3+
*
4+
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*/
25+
package de.bluecolored.bluemap.common.addons;
26+
27+
import de.bluecolored.bluemap.common.config.ConfigurationException;
28+
import de.bluecolored.bluemap.core.BlueMap;
29+
import de.bluecolored.bluemap.core.logger.Logger;
30+
import lombok.Singular;
31+
import org.jetbrains.annotations.Nullable;
32+
33+
import java.io.IOException;
34+
import java.net.URL;
35+
import java.net.URLClassLoader;
36+
import java.nio.file.Files;
37+
import java.nio.file.Path;
38+
import java.util.*;
39+
import java.util.concurrent.ConcurrentHashMap;
40+
import java.util.stream.Collectors;
41+
import java.util.stream.Stream;
42+
43+
public final class AddonLoader {
44+
public static final AddonLoader INSTANCE = new AddonLoader();
45+
46+
private final Map<String, LoadedAddon> loadedAddons = new ConcurrentHashMap<>();
47+
48+
public void tryLoadAddons(Path root) {
49+
if (!Files.exists(root)) return;
50+
try (Stream<Path> files = Files.list(root)) {
51+
52+
// find all addons and load addon-info
53+
Map<String, Addon> availableAddons = files
54+
.filter(Files::isRegularFile)
55+
.filter(f -> f.getFileName().toString().endsWith(".jar"))
56+
.map(this::tryLoadAddonInfo)
57+
.filter(Objects::nonNull)
58+
.collect(Collectors.toMap(addon -> addon.getAddonInfo().getId(), addon -> addon));
59+
60+
// remove addons that have missing required dependencies
61+
while (!availableAddons.isEmpty()) {
62+
Addon addonToRemove = availableAddons.values().stream()
63+
.filter(a -> !availableAddons.keySet().containsAll(a.getAddonInfo().getDependencies()))
64+
.findAny()
65+
.orElse(null);
66+
if (addonToRemove == null) break;
67+
String id = addonToRemove.getAddonInfo().getId();
68+
availableAddons.remove(id);
69+
new ConfigurationException("Missing required dependencies %s to load addon '%s' (%s)".formatted(
70+
Arrays.toString(addonToRemove.getAddonInfo().getDependencies().toArray(String[]::new)),
71+
id,
72+
addonToRemove.getJarFile()
73+
)).printLog(Logger.global);
74+
}
75+
76+
// topography sort and load addons based on their dependencies
77+
Map<String, Long> dependenciesToLoad = new HashMap<>();
78+
Queue<String> loadNext = new ArrayDeque<>();
79+
for (Addon addon : availableAddons.values()) {
80+
long dependencyCount =
81+
addon.getAddonInfo().getDependencies().size() +
82+
addon.getAddonInfo().getSoftDependencies().stream()
83+
.filter(availableAddons::containsKey)
84+
.count();
85+
String id = addon.getAddonInfo().getId();
86+
if (dependencyCount == 0) loadNext.add(id);
87+
else dependenciesToLoad.put(id, dependencyCount);
88+
}
89+
90+
while (!loadNext.isEmpty()) {
91+
String id = loadNext.poll();
92+
Addon addon = availableAddons.get(id);
93+
94+
try {
95+
loadAddon(addon);
96+
for (Addon dependant : availableAddons.values()) {
97+
AddonInfo info = dependant.getAddonInfo();
98+
if (info.getDependencies().contains(id) || info.getSoftDependencies().contains(id)) {
99+
Long count = dependenciesToLoad.get(info.getId());
100+
if (count == null) continue;
101+
if (--count <= 0) {
102+
dependenciesToLoad.remove(info.getId());
103+
loadNext.add(info.getId());
104+
} else {
105+
dependenciesToLoad.put(info.getId(), count);
106+
}
107+
}
108+
}
109+
} catch (ConfigurationException ex) {
110+
new ConfigurationException("Failed to load addon '%s' (%s)".formatted(id, addon.getJarFile()), ex)
111+
.printLog(Logger.global);
112+
}
113+
}
114+
115+
// failed to resolve dependencies, possibly a cyclic reference
116+
// try to load anyway in case a soft dependency is involved
117+
for (String id : dependenciesToLoad.keySet()) {
118+
Addon addon = availableAddons.remove(id);
119+
try {
120+
if (addon != null) loadAddon(addon);
121+
} catch (ConfigurationException ex) {
122+
new ConfigurationException("Failed to load addon '%s' (%s)".formatted(id, addon.getJarFile()), ex)
123+
.printLog(Logger.global);
124+
}
125+
}
126+
127+
} catch (IOException e) {
128+
Logger.global.logError("Failed to load addons from '%s'".formatted(root), e);
129+
}
130+
}
131+
132+
private @Nullable Addon tryLoadAddonInfo(Path jarFile) {
133+
try {
134+
AddonInfo addonInfo = AddonInfo.load(jarFile);
135+
if (addonInfo == null) return null;
136+
return new Addon(addonInfo, jarFile);
137+
} catch (ConfigurationException e) {
138+
new ConfigurationException("Failed to load addon info from '%s'.".formatted(jarFile), e)
139+
.printLog(Logger.global);
140+
return null;
141+
}
142+
}
143+
144+
private synchronized void loadAddon(Addon addon) throws ConfigurationException {
145+
AddonInfo addonInfo = addon.getAddonInfo();
146+
Path jarFile = addon.getJarFile();
147+
148+
if (loadedAddons.containsKey(addonInfo.getId())) return;
149+
Logger.global.logInfo("Loading BlueMap Addon: %s (%s)".formatted(addonInfo.getId(), jarFile));
150+
151+
try {
152+
Set<ClassLoader> dependencyClassLoaders = new LinkedHashSet<>();
153+
for (String dependencyId : addon.getAddonInfo().getDependencies()) {
154+
LoadedAddon loadedAddon = loadedAddons.get(dependencyId);
155+
if (loadedAddon == null) throw new IllegalStateException("Required dependency '%s' is not loaded."
156+
.formatted(addon.getAddonInfo().getId()));
157+
dependencyClassLoaders.add(loadedAddon.getClassLoader());
158+
}
159+
for (String dependencyId : addon.getAddonInfo().getSoftDependencies()) {
160+
LoadedAddon loadedAddon = loadedAddons.get(dependencyId);
161+
if (loadedAddon == null) continue;
162+
dependencyClassLoaders.add(loadedAddon.getClassLoader());
163+
}
164+
165+
ClassLoader parent = BlueMap.class.getClassLoader();
166+
if (!dependencyClassLoaders.isEmpty())
167+
parent = new CombinedClassLoader(parent, dependencyClassLoaders.toArray(ClassLoader[]::new));
168+
169+
ClassLoader addonClassLoader = new URLClassLoader(
170+
new URL[]{ jarFile.toUri().toURL() },
171+
parent
172+
);
173+
Class<?> entrypointClass = addonClassLoader.loadClass(addonInfo.getEntrypoint());
174+
175+
// create addon instance
176+
Object instance = entrypointClass.getConstructor().newInstance();
177+
LoadedAddon loadedAddon = new LoadedAddon(
178+
addonInfo,
179+
jarFile,
180+
addonClassLoader,
181+
instance
182+
);
183+
184+
loadedAddons.put(addonInfo.getId(), loadedAddon);
185+
186+
// run addon
187+
if (instance instanceof Runnable runnable)
188+
runnable.run();
189+
190+
} catch (Exception e) {
191+
throw new ConfigurationException("There was an exception trying to initialize the addon!", e);
192+
}
193+
}
194+
195+
}

0 commit comments

Comments
 (0)