diff --git a/.gitignore b/.gitignore index 2520df2c..fdf24b15 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ eclipse run resources/__pycache__ libs +tmp # Files from Forge MDK forge*changelog.txt diff --git a/build.gradle b/build.gradle index 67add5cf..cf20a399 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'maven-publish' -version = '0.5.19-dev.7' +version = '0.8.0-dev.0' group = 'com.eerussianguy.blazemap' // http://maven.apache.org/guides/mini/guide-naming-conventions.html archivesBaseName = "BlazeMap-${minecraft_version}" @@ -76,6 +76,15 @@ dependencies { // implementation fg.deobf("_:TerraFirmaCraft:Forge-1.18.2-2.1.11-beta") // implementation fg.deobf("_:Cartography:1.18.2-0.4.0-alpha3") + implementation fg.deobf("_:ftb-chunks:${ftbchunks_version}") + implementation fg.deobf("_:ftb-teams:${ftbteams_version}") + implementation fg.deobf("_:ftb-library:$ftblib_version") + implementation fg.deobf("_:architectury:${architectury_version}") + + // implementation fg.deobf("_:journeymap:1.20.1-5.10.0-forge") + // implementation fg.deobf("_:Xaeros_Minimap:24.2.0_Forge_1.20") + // implementation fg.deobf("_:XaerosWorldMap:1.38.8_Forge_1.20") + // Apply Mixin AP annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' diff --git a/gradle.properties b/gradle.properties index 88344633..c8e80b7c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,13 @@ # This is required to provide enough memory for the Minecraft decompilation process. org.gradle.jvmargs=-Xmx4G org.gradle.daemon=false +mappings_channel=official +mappings_version=1.18.2 + minecraft_version=1.18.2 forge_version=40.2.17 -mappings_channel=official -mappings_version=1.18.2 \ No newline at end of file + +ftbchunks_version=forge-1802.3.19-build.362 +ftbteams_version=forge-1802.2.11-build.107 +ftblib_version=forge-1802.3.11-build.177 +architectury_version=4.12.94-forge \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/BlazeMap.java b/src/main/java/com/eerussianguy/blazemap/BlazeMap.java index 6ec1fe3b..ece666b8 100644 --- a/src/main/java/com/eerussianguy/blazemap/BlazeMap.java +++ b/src/main/java/com/eerussianguy/blazemap/BlazeMap.java @@ -1,5 +1,8 @@ package com.eerussianguy.blazemap; +import java.util.List; + +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.IExtensionPoint; @@ -9,13 +12,16 @@ import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.loading.FMLEnvironment; -import com.eerussianguy.blazemap.api.BlazeMapAPI; import com.eerussianguy.blazemap.config.BlazeMapConfig; -import com.eerussianguy.blazemap.engine.client.BlazeMapClientEngine; -import com.eerussianguy.blazemap.engine.server.BlazeMapServerEngine; +import com.eerussianguy.blazemap.engine.RegistryController; +import com.eerussianguy.blazemap.engine.client.ClientEngine; +import com.eerussianguy.blazemap.engine.server.ServerEngine; import com.eerussianguy.blazemap.feature.BlazeMapCommandsClient; import com.eerussianguy.blazemap.feature.BlazeMapFeaturesClient; import com.eerussianguy.blazemap.feature.BlazeMapFeaturesCommon; +import com.eerussianguy.blazemap.integration.KnownMods; +import com.eerussianguy.blazemap.integration.ModIntegration; +import com.eerussianguy.blazemap.integration.ftbchunks.FTBChunksPlugin; import com.mojang.logging.LogUtils; import org.slf4j.Logger; @@ -28,6 +34,14 @@ public class BlazeMap { public static final String MOD_ID = "blazemap"; public static final String MOD_NAME = "Blaze Map"; + public static final List INTEGRATIONS = List.of( + new FTBChunksPlugin() + ); + + public static ResourceLocation resource(String name) { + return new ResourceLocation(MOD_ID, name); + } + public BlazeMap() { ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> "Nothing", (remote, isServer) -> true)); final IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus(); @@ -43,18 +57,18 @@ public BlazeMap() { // DebuggingEventHandler.init(); } else { - // These are forbidden in the dedicated server. + // Client side objects are forbidden in the dedicated server. // The others are frozen by the RegistryController when the time comes. - BlazeMapAPI.LAYERS.freeze(); - BlazeMapAPI.MAPTYPES.freeze(); - BlazeMapAPI.OBJECT_RENDERERS.freeze(); + RegistryController.freezeClientRegistries(); } } public void setup(FMLCommonSetupEvent event) { + KnownMods.init(); + // We are client side, enable client engine. Required on client. if(FMLEnvironment.dist == Dist.CLIENT) { - BlazeMapClientEngine.init(); + ClientEngine.init(); } // Regardless of side, server engine is optional. @@ -62,7 +76,7 @@ public void setup(FMLCommonSetupEvent event) { // So removing the mod to disable the server engine will not be an option. // For now, though, there are no other server features. if(BlazeMapConfig.COMMON.enableServerEngine.get()){ - BlazeMapServerEngine.init(); + ServerEngine.init(); } // Initialize common sided features @@ -71,9 +85,14 @@ public void setup(FMLCommonSetupEvent event) { // Initialize client-only features if(FMLEnvironment.dist == Dist.CLIENT) { BlazeMapFeaturesClient.initMapping(); + BlazeMapFeaturesClient.initOverlays(); BlazeMapFeaturesClient.initMaps(); BlazeMapFeaturesClient.initWaypoints(); BlazeMapCommandsClient.init(); } + + for(var integration : INTEGRATIONS) { + integration.init(); + } } } diff --git a/src/main/java/com/eerussianguy/blazemap/FMLEventHandler.java b/src/main/java/com/eerussianguy/blazemap/FMLEventHandler.java index 3b078137..d4372f43 100644 --- a/src/main/java/com/eerussianguy/blazemap/FMLEventHandler.java +++ b/src/main/java/com/eerussianguy/blazemap/FMLEventHandler.java @@ -5,7 +5,7 @@ import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -import com.eerussianguy.blazemap.feature.Overlays; +import com.eerussianguy.blazemap.feature.IngameOverlays; public class FMLEventHandler { @@ -18,10 +18,10 @@ public static void init() { } private static void clientSetup(final FMLClientSetupEvent event) { - Overlays.reload(); + IngameOverlays.reload(); } private static void onConfigReload(ModConfigEvent.Reloading event) { - Overlays.reload(); + IngameOverlays.reload(); } } diff --git a/src/main/java/com/eerussianguy/blazemap/gui/BlazeGui.java b/src/main/java/com/eerussianguy/blazemap/__deprecated/BlazeGui.java similarity index 89% rename from src/main/java/com/eerussianguy/blazemap/gui/BlazeGui.java rename to src/main/java/com/eerussianguy/blazemap/__deprecated/BlazeGui.java index 2e710a46..7c0d1f05 100644 --- a/src/main/java/com/eerussianguy/blazemap/gui/BlazeGui.java +++ b/src/main/java/com/eerussianguy/blazemap/__deprecated/BlazeGui.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.gui; +package com.eerussianguy.blazemap.__deprecated; import javax.annotation.Nonnull; @@ -11,16 +11,17 @@ import net.minecraft.network.chat.TextComponent; import net.minecraft.resources.ResourceLocation; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; -import com.eerussianguy.blazemap.util.RenderHelper; +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.Tesselator; +@Deprecated public abstract class BlazeGui extends Screen { private static final TextComponent EMPTY = new TextComponent(""); - public static final ResourceLocation SLOT = Helpers.identifier("textures/gui/slot.png"); - public static final ResourceLocation GUI = Helpers.identifier("textures/gui/gui.png"); + public static final ResourceLocation SLOT = BlazeMap.resource("textures/gui/slot.png"); + public static final ResourceLocation GUI = BlazeMap.resource("textures/gui/gui.png"); protected final RenderType background, slot; protected int guiWidth, guiHeight; diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/LayerButton.java b/src/main/java/com/eerussianguy/blazemap/__deprecated/LayerButton.java similarity index 62% rename from src/main/java/com/eerussianguy/blazemap/feature/maps/LayerButton.java rename to src/main/java/com/eerussianguy/blazemap/__deprecated/LayerButton.java index 3d53cced..fb08220c 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/LayerButton.java +++ b/src/main/java/com/eerussianguy/blazemap/__deprecated/LayerButton.java @@ -1,26 +1,33 @@ -package com.eerussianguy.blazemap.feature.maps; +package com.eerussianguy.blazemap.__deprecated; +import net.minecraft.ChatFormatting; import net.minecraft.client.gui.components.ImageButton; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.RenderHelper; -import com.mojang.blaze3d.systems.RenderSystem; +import com.eerussianguy.blazemap.feature.maps.ui.MapHost; +import com.eerussianguy.blazemap.integration.KnownMods; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; import com.mojang.blaze3d.vertex.PoseStack; +@Deprecated public class LayerButton extends ImageButton { private final Key key; private final MapType parent; - private final IMapHost host; + private final MapHost host; + private final Component owner; - public LayerButton(int px, int py, int w, int h, Key key, MapType parent, IMapHost host) { + public LayerButton(int px, int py, int w, int h, Key key, MapType parent, MapHost host) { super(px, py, w, h, 0, 0, 0, key.value().getIcon(), w, h, button -> { host.toggleLayer(key); }, key.value().getName()); this.key = key; this.parent = parent; + this.owner = new TextComponent(KnownMods.getOwnerName(key)).withStyle(ChatFormatting.BLUE); this.host = host; checkVisible(); } @@ -37,8 +44,8 @@ public void render(PoseStack stack, int mx, int my, float partial) { @Override public void renderToolTip(PoseStack stack, int x, int y) { - RenderSystem.setShaderColor(1, 1, 1, 1); - host.drawTooltip(stack, key.value().getName(), x, y); + //RenderSystem.setShaderColor(1, 1, 1, 1); + //host.drawTooltip(stack, x, y, key.value().getName(), owner); } public void checkVisible() { diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/MapTypeButton.java b/src/main/java/com/eerussianguy/blazemap/__deprecated/MapTypeButton.java similarity index 58% rename from src/main/java/com/eerussianguy/blazemap/feature/maps/MapTypeButton.java rename to src/main/java/com/eerussianguy/blazemap/__deprecated/MapTypeButton.java index 916ee854..c5686a13 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/MapTypeButton.java +++ b/src/main/java/com/eerussianguy/blazemap/__deprecated/MapTypeButton.java @@ -1,39 +1,43 @@ -package com.eerussianguy.blazemap.feature.maps; +package com.eerussianguy.blazemap.__deprecated; +import net.minecraft.ChatFormatting; import net.minecraft.client.gui.components.ImageButton; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.maps.MapType; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.RenderHelper; -import com.mojang.blaze3d.systems.RenderSystem; +import com.eerussianguy.blazemap.feature.maps.ui.MapHost; +import com.eerussianguy.blazemap.integration.KnownMods; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; import com.mojang.blaze3d.vertex.PoseStack; +@Deprecated public class MapTypeButton extends ImageButton { private final Key key; - private final IMapHost host; + private final MapHost host; + private final Component owner; - public MapTypeButton(int px, int py, int w, int h, Key key, IMapHost host) { + public MapTypeButton(int px, int py, int w, int h, Key key, MapHost host) { super(px, py, w, h, 0, 0, 0, key.value().getIcon(), w, h, button -> { host.setMapType(key.value()); - for(GuiEventListener widget : host.getChildren()) { + /*for(GuiEventListener widget : host.getChildren()) { if(widget instanceof LayerButton lb) { lb.checkVisible(); } - } + }*/ }, key.value().getName()); this.host = host; this.key = key; + this.owner = new TextComponent(KnownMods.getOwnerName(key)).withStyle(ChatFormatting.BLUE); } @Override public void renderToolTip(PoseStack stack, int x, int y) { - RenderSystem.setShaderColor(1, 1, 1, 1); - TranslatableComponent component = key.value().getName(); - host.drawTooltip(stack, component, x, y); + //RenderSystem.setShaderColor(1, 1, 1, 1); + //host.drawTooltip(stack, x, y, key.value().getName(), owner); } @Override diff --git a/src/main/java/com/eerussianguy/blazemap/api/BlazeMapAPI.java b/src/main/java/com/eerussianguy/blazemap/api/BlazeMapAPI.java index d55b159e..83572794 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/BlazeMapAPI.java +++ b/src/main/java/com/eerussianguy/blazemap/api/BlazeMapAPI.java @@ -2,6 +2,7 @@ import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.Overlay; import com.eerussianguy.blazemap.api.markers.ObjectRenderer; import com.eerussianguy.blazemap.api.pipeline.*; @@ -28,5 +29,6 @@ public class BlazeMapAPI { public static final BlazeRegistry LAYERS = new BlazeRegistry<>(); public static final BlazeRegistry MAPTYPES = new BlazeRegistry<>(); + public static final BlazeRegistry OVERLAYS = new BlazeRegistry<>(); public static final BlazeRegistry> OBJECT_RENDERERS = new BlazeRegistry<>(); } diff --git a/src/main/java/com/eerussianguy/blazemap/api/BlazeMapReferences.java b/src/main/java/com/eerussianguy/blazemap/api/BlazeMapReferences.java index 0f474d04..860ae981 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/BlazeMapReferences.java +++ b/src/main/java/com/eerussianguy/blazemap/api/BlazeMapReferences.java @@ -6,6 +6,7 @@ import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.Overlay; import com.eerussianguy.blazemap.api.markers.ObjectRenderer; import com.eerussianguy.blazemap.api.pipeline.Collector; import com.eerussianguy.blazemap.api.pipeline.DataType; @@ -54,6 +55,17 @@ public static class MapTypes { public static final Key NETHER = new Key<>(MAPTYPES, MODID, "nether"); } + public static class Overlays { + public static final Key GRID = new Key<>(OVERLAYS, MODID, "grid"); + public static final Key PLAYERS = new Key<>(OVERLAYS, MODID, "players"); + public static final Key NPCS = new Key<>(OVERLAYS, MODID, "npcs"); + public static final Key ANIMALS = new Key<>(OVERLAYS, MODID, "animals"); + public static final Key ENEMIES = new Key<>(OVERLAYS, MODID, "enemies"); + public static final Key WAYPOINTS = new Key<>(OVERLAYS, MODID, "waypoints"); + + public static final Key FTBCHUNKS = new Key<>(OVERLAYS, MODID, "ftbchunks"); + } + public static class ObjectRenderers { public static final Key> DEFAULT = new Key<>(OBJECT_RENDERERS, MODID, "default"); } diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/BlazeRegistriesFrozenEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/BlazeRegistriesFrozenEvent.java new file mode 100644 index 00000000..43f3c1b8 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/event/BlazeRegistriesFrozenEvent.java @@ -0,0 +1,11 @@ +package com.eerussianguy.blazemap.api.event; + +import net.minecraftforge.eventbus.api.Event; + +/** + * Fired immediately after all registries have been frozen. + * Used by systems that need to be sure the registries will no longer be changed. + * + * @author LordFokas + */ +public class BlazeRegistriesFrozenEvent extends Event {} diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/BlazeRegistryEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/BlazeRegistryEvent.java index dbc58222..ce20d0e1 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/event/BlazeRegistryEvent.java +++ b/src/main/java/com/eerussianguy/blazemap/api/event/BlazeRegistryEvent.java @@ -6,6 +6,7 @@ import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.Overlay; import com.eerussianguy.blazemap.api.markers.ObjectRenderer; import com.eerussianguy.blazemap.api.pipeline.*; @@ -64,6 +65,12 @@ public MapTypeRegistryEvent() { } } + public static class OverlayRegistryEvent extends BlazeRegistryEvent { + public OverlayRegistryEvent() { + super(BlazeMapAPI.OVERLAYS); + } + } + public static class ObjectRendererRegistryEvent extends BlazeRegistryEvent> { public ObjectRendererRegistryEvent() { super(BlazeMapAPI.OBJECT_RENDERERS); diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/ClientEngineEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/ClientEngineEvent.java new file mode 100644 index 00000000..73b423b8 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/event/ClientEngineEvent.java @@ -0,0 +1,47 @@ +package com.eerussianguy.blazemap.api.event; + +import net.minecraftforge.eventbus.api.Event; + +import com.eerussianguy.blazemap.api.util.StorageAccess; + +/** + * Client Engine lifecycle events. + * + * @author LordFokas + */ +public abstract class ClientEngineEvent extends Event { + /** + * The internal ID used to represent this server + */ + public final String serverID; + + /** + * The file storage where Blaze Map stores all the data for this server + */ + public final StorageAccess.ServerStorage serverStorage; + + /** + * If the server has Blaze Map present + */ + public final boolean serverHasBlazeMap; + + protected ClientEngineEvent(String serverID, StorageAccess.ServerStorage storage, boolean serverHasBlazeMap) { + this.serverID = serverID; + this.serverStorage = storage; + this.serverHasBlazeMap = serverHasBlazeMap; + } + + /** Fired by the Blaze Map engine after the game connects to a new server, after the engine starts. */ + public static class EngineStartingEvent extends ClientEngineEvent { + public EngineStartingEvent(String serverID, StorageAccess.ServerStorage storage, boolean serverHasBlazeMap) { + super(serverID, storage, serverHasBlazeMap); + } + } + + /** Fired by the Blaze Map engine before the game disconnects from a server, before the engine stops. */ + public static class EngineStoppingEvent extends ClientEngineEvent { + public EngineStoppingEvent(String serverID, StorageAccess.ServerStorage storage, boolean serverHasBlazeMap) { + super(serverID, storage, serverHasBlazeMap); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/ComponentOrderingEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/ComponentOrderingEvent.java new file mode 100644 index 00000000..1cc96169 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/event/ComponentOrderingEvent.java @@ -0,0 +1,159 @@ +package com.eerussianguy.blazemap.api.event; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.Set; + +import net.minecraftforge.eventbus.api.Event; + +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.NamedMapComponent; +import com.eerussianguy.blazemap.api.maps.Overlay; + +/** + * A generic base event to handle manipulation of NamedMapComponent sets. + * Once constructed with a set of components, it gives subscribers the chance to alter the list contents + * as well as the order of its elements. + * Refer to this event's subclasses for intended usage. + * + * @param the type of NamedMapComponent + * + * @author LordFokas + */ +public abstract class ComponentOrderingEvent> extends Event { + private LinkedHashSet> orderedSet; + private ArrayList> list; + private final String type; + + protected ComponentOrderingEvent(LinkedHashSet> orderedSet, String type) { + this.orderedSet = orderedSet; + this.list = new ArrayList<>(orderedSet); + this.type = type; + } + + /** + * Fired after registry freeze to expand MapTypes. + * This event is a chance for addons to contribute to or modify the layer stack in maps they do not own. + * Can also be used as a loosely coupled way to conditionally add integration layers to your own maps. + */ + public static class LayerOrderingEvent extends ComponentOrderingEvent { + public final BlazeRegistry.Key id; + + + public LayerOrderingEvent(BlazeRegistry.Key id, LinkedHashSet> layers) { + super(layers, "Layer"); + this.id = id; + } + } + + /** + * Fired after registry freeze to add Overlays to the maps. + * Even though they have been registered, Overlays will not be available in the maps without being added here. + * This even is also a chance for addons to modify the overlay set or the rendering order. + */ + public static class OverlayOrderingEvent extends ComponentOrderingEvent { + public OverlayOrderingEvent(LinkedHashSet> orderedSet) { + super(orderedSet, "Overlay"); + } + } + + /** Adds your component to the top of the stack */ + public void add(BlazeRegistry.Key component) { + if(component == null) throw new IllegalArgumentException(type + " cannot be null"); + if(list.contains(component)) throw new IllegalStateException(type + " already in set"); + + list.add(component); + } + + /** Adds multiple components, in order, to the top of the stack */ + @SafeVarargs + public final void add(BlazeRegistry.Key... components) { + if(components == null || components.length == 0) throw new IllegalArgumentException("components must not be null or zero-length"); + + for(var component : components) { + add(component); + } + } + + /** + * Adds your component to the next position immediately after ALL the targets. + * If none of the targets is found, does nothing and returns false. + * + * @param component the component you wish to add + * @param targets the targets after which your component will be added + * @return true if added, false if not + */ + public boolean addAfter(BlazeRegistry.Key component, Set> targets) { + if(component == null) throw new IllegalArgumentException(type + " cannot be null"); + if(targets.size() == 0) throw new IllegalArgumentException("No targets provided"); + if(list.contains(component)) throw new IllegalStateException(type + " already in set"); + + int last = -1; + for(var target : targets) { + int index = list.indexOf(target); + if(index > last){ + last = index; + } + } + + if(last == -1) return false; + list.add(last+1, component); + return true; + } + + /** + * Adds your component to the next position immediately before ALL the targets. + * If none of the targets is found, does nothing and returns false. + * + * @param component the component you wish to add + * @param targets the targets before which your component will be added + * @return true if added, false if not + */ + public boolean addBefore(BlazeRegistry.Key component, Set> targets) { + if(component == null) throw new IllegalArgumentException(type + " cannot be null"); + if(targets.size() == 0) throw new IllegalArgumentException("No targets provided"); + if(list.contains(component)) throw new IllegalStateException(type + " already in set"); + + int first = Integer.MAX_VALUE; + for(var target : targets) { + int index = list.indexOf(target); + if(index == -1) continue; + if(index < first){ + first = index; + } + } + + if(first == Integer.MAX_VALUE) return false; + list.add(first, component); + return true; + } + + /** Removes a component from the stack */ + public boolean remove(BlazeRegistry.Key component) { + if(component == null) throw new IllegalArgumentException(type + " cannot be null"); + + return list.remove(component); + } + + /** Replaces one component with another */ + public boolean replace(BlazeRegistry.Key oldComponent, BlazeRegistry.Key newComponent) { + if(oldComponent == null) throw new IllegalArgumentException("Old " + type + " cannot be null"); + if(newComponent == null) throw new IllegalArgumentException("New " + type + " cannot be null"); + if(list.contains(newComponent)) throw new IllegalStateException(type + " already in set"); + + int index = list.indexOf(oldComponent); + if(index == -1) return false; + list.set(index, newComponent); + return true; + } + + /** Called internally by the publisher. Do not call this method yourself. */ + public void finish() { + orderedSet.clear(); + orderedSet.addAll(list); + orderedSet = null; + list = null; + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/DimensionChangedEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/DimensionChangedEvent.java index dbe1ee00..cda19875 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/event/DimensionChangedEvent.java +++ b/src/main/java/com/eerussianguy/blazemap/api/event/DimensionChangedEvent.java @@ -11,10 +11,8 @@ import com.eerussianguy.blazemap.api.maps.DimensionTileStorage; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; -import com.eerussianguy.blazemap.api.markers.MapLabel; -import com.eerussianguy.blazemap.api.markers.Waypoint; -import com.eerussianguy.blazemap.api.util.IStorageAccess; +import com.eerussianguy.blazemap.api.markers.MarkerStorage; +import com.eerussianguy.blazemap.api.util.StorageAccess; /** * Fired after the client enters a new dimension.
@@ -59,17 +57,12 @@ public class DimensionChangedEvent extends Event { /** * Volatile storage containing all the addon map labels for this dimension */ - public final IMarkerStorage.Layered labels; - - /** - * Permanent storage containing all the player waypoints for this dimension - */ - public final IMarkerStorage waypoints; + public final MarkerStorage.MapComponentStorage labels; /** * Client file storage where all map data for this dimension is stored */ - public final IStorageAccess dimensionStorage; + public final StorageAccess dimensionStorage; public DimensionChangedEvent( ResourceKey dimension, @@ -77,9 +70,8 @@ public DimensionChangedEvent( Set> layers, DimensionTileNotifications notifications, DimensionTileStorage tiles, - IMarkerStorage.Layered labels, - IMarkerStorage waypoints, - IStorageAccess storageAccess + MarkerStorage.MapComponentStorage labels, + StorageAccess storageAccess ) { this.dimension = dimension; this.availableMapTypes = mapTypes; @@ -87,7 +79,6 @@ public DimensionChangedEvent( this.tileNotifications = notifications; this.tileStorage = tiles; this.labels = labels; - this.waypoints = waypoints; this.dimensionStorage = storageAccess; } } diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/MapMenuSetupEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/MapMenuSetupEvent.java index 9213edb6..13634868 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/event/MapMenuSetupEvent.java +++ b/src/main/java/com/eerussianguy/blazemap/api/event/MapMenuSetupEvent.java @@ -7,22 +7,29 @@ import java.util.function.Consumer; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; import net.minecraftforge.eventbus.api.Event; import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.Overlay; public class MapMenuSetupEvent extends Event { public final MenuFolder root; public final List> layers; + public final List> overlays; + public final ResourceKey dimension; public final int blockPosX, blockPosZ; public final int chunkPosX, chunkPosZ; public final int regionPosX, regionPosZ; - public MapMenuSetupEvent(MenuFolder root, List> layers, int blockPosX, int blockPosZ, int chunkPosX, int chunkPosZ, int regionPosX, int regionPosZ) { + public MapMenuSetupEvent(MenuFolder root, List> layers, List> overlays, ResourceKey dimension, int blockPosX, int blockPosZ, int chunkPosX, int chunkPosZ, int regionPosX, int regionPosZ) { this.root = root; this.layers = Collections.unmodifiableList(layers); + this.overlays = Collections.unmodifiableList(overlays); + this.dimension = dimension; this.blockPosX = blockPosX; this.blockPosZ = blockPosZ; this.chunkPosX = chunkPosX; diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/ServerEngineEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/ServerEngineEvent.java new file mode 100644 index 00000000..7f0a7d7e --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/event/ServerEngineEvent.java @@ -0,0 +1,33 @@ +package com.eerussianguy.blazemap.api.event; + +import net.minecraftforge.eventbus.api.Event; + +import com.eerussianguy.blazemap.api.util.StorageAccess; + +/** + * Server Engine lifecycle events. + * + * @author LordFokas + */ +public abstract class ServerEngineEvent extends Event { + /** The file storage where Blaze Map stores all the data for this server */ + public final StorageAccess.ServerStorage serverStorage; + + protected ServerEngineEvent(StorageAccess.ServerStorage storage) { + this.serverStorage = storage; + } + + /** Fired by the Blaze Map engine, after the server engine starts. */ + public static class EngineStartingEvent extends ServerEngineEvent { + public EngineStartingEvent(StorageAccess.ServerStorage storage) { + super(storage); + } + } + + /** Fired by the Blaze Map engine, before the server engine stops. */ + public static class EngineStoppingEvent extends ServerEngineEvent { + public EngineStoppingEvent(StorageAccess.ServerStorage storage) { + super(storage); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/ServerJoinedEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/ServerJoinedEvent.java deleted file mode 100644 index c68395c8..00000000 --- a/src/main/java/com/eerussianguy/blazemap/api/event/ServerJoinedEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.eerussianguy.blazemap.api.event; - -import java.util.Objects; - -import net.minecraftforge.eventbus.api.Event; - -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; -import com.eerussianguy.blazemap.api.markers.IStorageFactory; -import com.eerussianguy.blazemap.api.markers.Waypoint; -import com.eerussianguy.blazemap.api.util.IStorageAccess; - -/** - * Fired by the Blaze Map engine after the game connects to a new server.
- * The engine is not yet ready to serve mapping related requests, for that see DimensionChangedEvent. - * - * @author LordFokas - */ -public class ServerJoinedEvent extends Event { - /** - * The internal ID used to represent this server - */ - public final String serverID; - - /** - * The file storage where Blaze Map stores all the data for this server - */ - public final IStorageAccess serverStorage; - - /** - * The factory that creates IMarkerStorage instances to permanently store waypoints for this server - */ - private IStorageFactory> waypointStorageFactory; - - /** - * If the server we connected to has Blaze Map present - */ - public final boolean serverHasBlazeMap; - - public ServerJoinedEvent(String serverID, IStorageAccess storage, boolean serverHasBlazeMap) { - this.serverID = serverID; - this.serverStorage = storage; - this.serverHasBlazeMap = serverHasBlazeMap; - this.waypointStorageFactory = (i, o, e) -> new IMarkerStorage.Dummy<>() {}; - } - - public IStorageFactory> getWaypointStorageFactory() { - return waypointStorageFactory; - } - - public void setWaypointStorageFactory(IStorageFactory> waypointStorageFactory) { - this.waypointStorageFactory = Objects.requireNonNull(waypointStorageFactory); - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/api/event/ServerPipelineInitEvent.java b/src/main/java/com/eerussianguy/blazemap/api/event/ServerPipelineInitEvent.java index 56faa931..71d9ac2a 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/event/ServerPipelineInitEvent.java +++ b/src/main/java/com/eerussianguy/blazemap/api/event/ServerPipelineInitEvent.java @@ -7,7 +7,7 @@ import net.minecraftforge.eventbus.api.Event; import com.eerussianguy.blazemap.api.pipeline.MasterDataDispatcher; -import com.eerussianguy.blazemap.api.util.IStorageAccess; +import com.eerussianguy.blazemap.api.util.StorageAccess; /** * Fired when the Blaze Map server engine initializes a pipeline to serve a new dimension. @@ -22,14 +22,14 @@ public class ServerPipelineInitEvent extends Event { /** * Server file storage where all map data for this dimension is stored */ - public final IStorageAccess dimensionStorage; + public final StorageAccess dimensionStorage; /** * The dispatcher that forwards MasterData updates to the clients */ private MasterDataDispatcher dispatcher; - public ServerPipelineInitEvent(ResourceKey dimension, IStorageAccess dimensionStorage, MasterDataDispatcher dispatcher) { + public ServerPipelineInitEvent(ResourceKey dimension, StorageAccess dimensionStorage, MasterDataDispatcher dispatcher) { this.dimension = dimension; this.dimensionStorage = dimensionStorage; this.dispatcher = dispatcher; diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/DimensionTileStorage.java b/src/main/java/com/eerussianguy/blazemap/api/maps/DimensionTileStorage.java index 5b072ead..f347eab0 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/maps/DimensionTileStorage.java +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/DimensionTileStorage.java @@ -4,7 +4,6 @@ import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.util.RegionPos; -import com.mojang.blaze3d.platform.NativeImage; @FunctionalInterface public interface DimensionTileStorage { @@ -16,7 +15,7 @@ public interface DimensionTileStorage { *
* WARNING!
*
- * In order to ensure thread safety please do all NativeImage related processing inside the Consumer code.
+ * In order to ensure thread safety please do all image related processing inside the Consumer code.
* DO NOT attempt to save a reference to the image to handle it later.
*
* You have been warned.
@@ -25,5 +24,5 @@ public interface DimensionTileStorage { * @throws IllegalArgumentException if the layer is not in the availableLayers Set. * @author LordFokas */ - void consumeTile(BlazeRegistry.Key layer, RegionPos region, TileResolution resolution, Consumer consumer); + void consumeTile(BlazeRegistry.Key layer, RegionPos region, TileResolution resolution, Consumer consumer); } \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/FakeLayer.java b/src/main/java/com/eerussianguy/blazemap/api/maps/FakeLayer.java deleted file mode 100644 index 92342502..00000000 --- a/src/main/java/com/eerussianguy/blazemap/api/maps/FakeLayer.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.eerussianguy.blazemap.api.maps; - -import net.minecraft.client.gui.components.Widget; -import net.minecraft.network.chat.TranslatableComponent; -import net.minecraft.resources.ResourceLocation; - -import com.eerussianguy.blazemap.api.BlazeRegistry; -import com.eerussianguy.blazemap.api.util.IDataSource; -import com.mojang.blaze3d.platform.NativeImage; - -/** - * A FakeLayer is an invisible layer that does not render in the map, for when you need a layer but you don't want a layer. - * FakeLayers are treated exactly the same way in every situation, with these differences: - * - They do not generate a tile, nor does the engine ask them to. - * - They are never considered opaque. This means they must have an icon as they are not expected to be the bottom layer of any map. - * - * In practical terms, what this means is that you can use FakeLayers to put markers on the map. - * Using multiple fake layers enables you to segregate markers, and disabling the layers on the side panel will hide only - * this set of markers. - * - * @author LordFokas - */ -public class FakeLayer extends Layer { - - public FakeLayer(BlazeRegistry.Key id, TranslatableComponent name, ResourceLocation icon) { - super(id, name, icon); - } - - @Override // can't render - public boolean renderTile(NativeImage tile, TileResolution resolution, IDataSource data, int xGridOffset, int zGridOffset) { - throw new UnsupportedOperationException("FakeLayers do not render: " + getID()); - } - - @Override // can't have a legend - public final Widget getLegendWidget() { - return null; - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/GhostLayer.java b/src/main/java/com/eerussianguy/blazemap/api/maps/GhostLayer.java new file mode 100644 index 00000000..cd4620bc --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/GhostLayer.java @@ -0,0 +1,36 @@ +package com.eerussianguy.blazemap.api.maps; + +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.util.DataSource; +import com.mojang.blaze3d.platform.NativeImage; + +/** + * A GhostLayer is an invisible layer that does not render in the map, for when you need a layer but you don't want a layer. + * GhostLayers are treated exactly the same way in every situation, with these differences: + * - They do not generate a tile, nor does the engine ask them to. + * - They are never considered opaque. This means they must have an icon as they are not expected to be the bottom layer of any map. + * + * In practical terms, what this means is that you can use GhostLayers to put markers on the map. + * Using multiple ghost layers enables you to segregate markers, and disabling the layers on the side panel will hide only + * this set of markers. + * + * @author LordFokas + */ +public class GhostLayer extends Layer { + public GhostLayer(BlazeRegistry.Key id, TranslatableComponent name, ResourceLocation icon) { + super(id, Type.INVISIBLE, name, icon, false); + } + + @Override // can't render + public boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset) { + throw new UnsupportedOperationException("GhostLayers do not render tiles: " + getID()); + } + + @Override // can't have a legend + public final Renderable getLegendWidget() { + return null; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/GhostOverlay.java b/src/main/java/com/eerussianguy/blazemap/api/maps/GhostOverlay.java new file mode 100644 index 00000000..a354b5d9 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/GhostOverlay.java @@ -0,0 +1,26 @@ +package com.eerussianguy.blazemap.api.maps; + +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.util.RegionPos; + +/** + * A GhostOverlay is an overlay type that does not render pixels in the map. + * Its use case is specifically to present markers (labels, waypoints, mobs) on the map. + * + * @author LordFokas + */ +public class GhostOverlay extends Overlay { + protected GhostOverlay(BlazeRegistry.Key id, TranslatableComponent name, ResourceLocation icon) { + super(id, Type.INVISIBLE, name, icon); + } + + @Override // GhostOverlays do not have map pixels + public final PixelSource getPixelSource(ResourceKey dimension, RegionPos region, TileResolution resolution) { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/IScreenSkipsMinimap.java b/src/main/java/com/eerussianguy/blazemap/api/maps/IScreenSkipsMinimap.java deleted file mode 100644 index 052708ff..00000000 --- a/src/main/java/com/eerussianguy/blazemap/api/maps/IScreenSkipsMinimap.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.eerussianguy.blazemap.api.maps; - -/** - * When a Screen implementing this interface is open the Minimap does not render. - */ -public interface IScreenSkipsMinimap { -} diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/Layer.java b/src/main/java/com/eerussianguy/blazemap/api/maps/Layer.java index e6dea18f..68ca63dc 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/maps/Layer.java +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/Layer.java @@ -5,18 +5,14 @@ import java.util.Set; import java.util.stream.Collectors; -import net.minecraft.client.gui.components.Widget; import net.minecraft.network.chat.TranslatableComponent; -import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.Level; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; -import com.eerussianguy.blazemap.api.BlazeRegistry.RegistryEntry; import com.eerussianguy.blazemap.api.pipeline.Consumer; import com.eerussianguy.blazemap.api.pipeline.DataType; import com.eerussianguy.blazemap.api.pipeline.MasterDatum; -import com.eerussianguy.blazemap.api.util.IDataSource; +import com.eerussianguy.blazemap.api.util.DataSource; import com.mojang.blaze3d.platform.NativeImage; /** @@ -31,35 +27,29 @@ * * @author LordFokas */ -public abstract class Layer implements RegistryEntry, Consumer { +public abstract class Layer extends NamedMapComponent implements Consumer { protected static final int OPAQUE = 0xFF000000; - private final Key id; private final Set>> inputs; - private final TranslatableComponent name; - private final ResourceLocation icon; private final boolean opaque; + public final Type type; @SafeVarargs - public Layer(Key id, TranslatableComponent name, Key>... inputs) { - this.id = id; - this.name = name; - this.icon = null; - this.inputs = Arrays.stream(inputs).collect(Collectors.toUnmodifiableSet()); - this.opaque = true; + public Layer(Key id, TranslatableComponent name, ResourceLocation icon, boolean opaque, Key>... inputs) { + this(id, Type.PHYSICAL, name, icon, opaque, inputs); } @SafeVarargs - public Layer(Key id, TranslatableComponent name, ResourceLocation icon, Key>... inputs) { - this.id = id; - this.name = name; - this.icon = icon; + Layer(Key id, Type type, TranslatableComponent name, ResourceLocation icon, boolean opaque, Key>... inputs) { + super(id, name, icon); + this.type = type; + if(type.isPipelined) { + if(inputs == null || inputs.length == 0) throw new IllegalArgumentException("Pipelined Layers must have non-zero input MD list"); + } else { + if(inputs != null && inputs.length > 0) throw new IllegalArgumentException("Non-Pipelined Layers must not have input MDs"); + } this.inputs = Arrays.stream(inputs).collect(Collectors.toUnmodifiableSet()); - this.opaque = false; - } - - public Key getID() { - return id; + this.opaque = opaque; } @Override @@ -67,38 +57,55 @@ public Set>> getInputIDs() { return inputs; } - public boolean shouldRenderInDimension(ResourceKey dimension) { - return true; - } - + /** + * isOpaque (alias for isBottomLayer) refers to the fact that bottom layers are opaque, and face different restrictions. + * 1 - opaque layers can only be the 1st layer in the map + * 1.1 - map therefore can only have 1 opaque layer + * 2 - layers in index >= 1 cannot be opaque + * 3 - opaque layers cannot be disabled (they are the map background) + */ public final boolean isOpaque() { return opaque; } - public abstract boolean renderTile(NativeImage tile, TileResolution resolution, IDataSource data, int xGridOffset, int zGridOffset); - - public TranslatableComponent getName() { - return name; + /** Alias for isOpaque */ + public final boolean isBottomLayer() { + return opaque; } - public ResourceLocation getIcon() { - return icon; - } + public abstract boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset); /** - * Used by the World Map (fullscreen map) to display a legend in the bottom right corner. - * The widget will be asked to render while translated to the corner of the screen, - * so it must render backwards (towards the left and up) in order to stay inside the screen. + * Used by the World Map (fullscreen map) to display a legend somewhere in the screen (at the layout's discretion) + * The renderable will be asked to render at its own 0,0 and the height and width are expected to be constant. * - * The translation to the corner may subtract a small margin to make all legends have a consistent margin with the border. - * - * This only applies to opaque (bottom) layers, which are the first layer of the current map type, + * This currently only applies to opaque (bottom) layers, which are the first layer of the current map type, * however not all such layers must have one and returning null is the default action. */ - public Widget getLegendWidget() { + public Renderable getLegendWidget() { return null; } + /** + * Determines the capabilities and limitations of layers. + */ + public enum Type { + PHYSICAL(true, true), + SYNTHETIC(false, true), + INVISIBLE(false, false); + + /** Whether this layer runs through the pipeline and saves to disk */ + public final boolean isPipelined; + + /** Whether this layer has pixels to display */ + public final boolean isVisible; + + Type(boolean isPipelined, boolean isVisible) { + this.isPipelined = isPipelined; + this.isVisible = isVisible; + } + } + // ================================================================================================================= // Common helper functions for easier layer rendering @@ -108,8 +115,13 @@ public Widget getLegendWidget() { * Automatically accounts for the fact chunk tile sizes vary with resolution. */ protected static void foreachPixel(TileResolution resolution, PixelConsumer consumer) { - for(int x = 0; x < resolution.chunkWidth; x++) { - for(int z = 0; z < resolution.chunkWidth; z++) { + // Note: It's more efficient overall to run the loops this way around with x as the inner loop. + // This is for the same reason the data format is expected to be `data[z][x]` down below. + // Namely, that the PNG tiles are constructed with the whole first row of x values read first, + // then the whole second row of x values, etc. + // The CPU cache can better optimise its memory fetches if they're accessed in order. + for(int z = 0; z < resolution.chunkWidth; z++) { + for(int x = 0; x < resolution.chunkWidth; x++) { consumer.accept(x, z); } } @@ -124,6 +136,8 @@ protected interface PixelConsumer { * When running at lower resolutions than FULL, this utility allows to collect all data points that will be rendered * into a single pixel. Meant to be used with ArrayAggregator or similar utilities, to aggregate the multiple data * points into a single value. + * + * Note: Data is expected to be in the format `data[z][x]` for CPU cache fetch optimisation reasons. */ protected static int[] relevantData(TileResolution resolution, int x, int z, int[][] data) { int[] objects = new int[resolution.pixelWidth * resolution.pixelWidth]; @@ -131,9 +145,9 @@ protected static int[] relevantData(TileResolution resolution, int x, int z, int z *= resolution.pixelWidth; int idx = 0; - for(int dx = 0; dx < resolution.pixelWidth; dx++) { - for(int dz = 0; dz < resolution.pixelWidth; dz++) { - objects[idx++] = data[dx + x][dz + z]; + for(int dz = 0; dz < resolution.pixelWidth; dz++) { + for(int dx = 0; dx < resolution.pixelWidth; dx++) { + objects[idx++] = data[dz + z][dx + x]; } } @@ -144,6 +158,8 @@ protected static int[] relevantData(TileResolution resolution, int x, int z, int * When running at lower resolutions than FULL, this utility allows to collect all data points that will be rendered * into a single pixel. Meant to be used with ArrayAggregator or similar utilities, to aggregate the multiple data * points into a single value. + * + * Note: Data is expected to be in the format `data[z][x]` for CPU cache fetch optimisation reasons. */ protected static float[] relevantData(TileResolution resolution, int x, int z, float[][] data) { float[] objects = new float[resolution.pixelWidth * resolution.pixelWidth]; @@ -151,9 +167,9 @@ protected static float[] relevantData(TileResolution resolution, int x, int z, f z *= resolution.pixelWidth; int idx = 0; - for(int dx = 0; dx < resolution.pixelWidth; dx++) { - for(int dz = 0; dz < resolution.pixelWidth; dz++) { - objects[idx++] = data[dx + x][dz + z]; + for(int dz = 0; dz < resolution.pixelWidth; dz++) { + for(int dx = 0; dx < resolution.pixelWidth; dx++) { + objects[idx++] = data[dz + z][dx + x]; } } @@ -164,6 +180,8 @@ protected static float[] relevantData(TileResolution resolution, int x, int z, f * When running at lower resolutions than FULL, this utility allows to collect all data points that will be rendered * into a single pixel. Meant to be used with ArrayAggregator or similar utilities, to aggregate the multiple data * points into a single value. + * + * Note: Data is expected to be in the format `data[z][x]` for CPU cache fetch optimisation reasons. */ @SuppressWarnings("unchecked") protected static T[] relevantData(TileResolution resolution, int x, int z, T[][] data, Class cls) { @@ -172,9 +190,9 @@ protected static T[] relevantData(TileResolution resolution, int x, int z, T z *= resolution.pixelWidth; int idx = 0; - for(int dx = 0; dx < resolution.pixelWidth; dx++) { - for(int dz = 0; dz < resolution.pixelWidth; dz++) { - objects[idx++] = data[dx + x][dz + z]; + for(int dz = 0; dz < resolution.pixelWidth; dz++) { + for(int dx = 0; dx < resolution.pixelWidth; dx++) { + objects[idx++] = data[dz + z][dx + x]; } } diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/MapType.java b/src/main/java/com/eerussianguy/blazemap/api/maps/MapType.java index 42434bad..9d5d6230 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/maps/MapType.java +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/MapType.java @@ -6,11 +6,12 @@ import java.util.Set; import net.minecraft.network.chat.TranslatableComponent; -import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.Level; + +import net.minecraftforge.common.MinecraftForge; import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.event.ComponentOrderingEvent.LayerOrderingEvent; /** * Each available map in Blaze Map is defined by MapType. @@ -20,38 +21,47 @@ * * @author LordFokas */ -public abstract class MapType implements BlazeRegistry.RegistryEntry { - private final BlazeRegistry.Key id; - private final Set> layers; - private final TranslatableComponent name; - private final ResourceLocation icon; +public class MapType extends NamedMapComponent { + private final LinkedHashSet> layers; + private final Set> layerView; + private boolean inflated; @SafeVarargs public MapType(BlazeRegistry.Key id, TranslatableComponent name, ResourceLocation icon, BlazeRegistry.Key... layers) { - this.id = id; - this.name = name; - this.icon = icon; - this.layers = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(layers))); + super(id, name, icon); + this.layers = new LinkedHashSet<>(Arrays.asList(layers)); + this.layerView = Collections.unmodifiableSet(this.layers); + checkValid(); } public Set> getLayers() { - return layers; + return layerView; } - @Override - public BlazeRegistry.Key getID() { - return id; - } + /** Used by the engine to give addons a chance to contribute to an external map type, do not call this method. */ + public void inflate() { + if(inflated) throw new IllegalStateException("MapType " + id + "already inflated"); + inflated = true; - public boolean shouldRenderInDimension(ResourceKey dimension) { - return true; - } + var event = new LayerOrderingEvent(id, layers); + MinecraftForge.EVENT_BUS.post(event); + event.finish(); - public TranslatableComponent getName() { - return name; + checkValid(); } - public ResourceLocation getIcon() { - return icon; + private void checkValid() { + if(layers.size() == 0) throw new IllegalStateException("MapType "+id+" must have at least 1 layer"); + + int index = 0; + for(var key : layers) { + var layer = key.value(); + if(index == 0) { + if(!layer.isBottomLayer()) throw new IllegalStateException("MapType "+id+" has non-opaque layer "+layer.getID()+" at the bottom (index 0)"); + } else { + if(layer.isBottomLayer()) throw new IllegalStateException("MapType "+id+" has opaque layer "+layer.getID()+" above the bottom (index "+index+")"); + } + index++; + } } } diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/NamedMapComponent.java b/src/main/java/com/eerussianguy/blazemap/api/maps/NamedMapComponent.java new file mode 100644 index 00000000..09f38411 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/NamedMapComponent.java @@ -0,0 +1,37 @@ +package com.eerussianguy.blazemap.api.maps; + +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.api.BlazeRegistry; + +/** Superclass for map components with common traits (Layer, MapType, Overlay) */ +public abstract class NamedMapComponent> implements BlazeRegistry.RegistryEntry { + protected final BlazeRegistry.Key id; + protected final TranslatableComponent name; + protected final ResourceLocation icon; + + protected NamedMapComponent(BlazeRegistry.Key id, TranslatableComponent name, ResourceLocation icon) { + this.id = id; + this.name = name; + this.icon = icon; + } + + public BlazeRegistry.Key getID() { + return id; + } + + public TranslatableComponent getName() { + return name; + } + + public ResourceLocation getIcon() { + return icon; + } + + public boolean shouldRenderInDimension(ResourceKey dimension) { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/Overlay.java b/src/main/java/com/eerussianguy/blazemap/api/maps/Overlay.java new file mode 100644 index 00000000..1ed31ad5 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/Overlay.java @@ -0,0 +1,44 @@ +package com.eerussianguy.blazemap.api.maps; + +import java.util.Collections; + +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.markers.Marker; +import com.eerussianguy.blazemap.api.util.RegionPos; + +public abstract class Overlay extends NamedMapComponent { + public final Type type; + + protected Overlay(BlazeRegistry.Key id, TranslatableComponent name, ResourceLocation icon) { + this(id, Type.RENDERABLE, name, icon); + } + + Overlay(BlazeRegistry.Key id, Type type, TranslatableComponent name, ResourceLocation icon) { + super(id, name, icon); + this.type = type; + } + + public abstract PixelSource getPixelSource(ResourceKey dimension, RegionPos region, TileResolution resolution); + + @SuppressWarnings("unchecked") + public Iterable> getMarkers(ClientLevel level, TileResolution resolution) { + return Collections.EMPTY_LIST; + } + + public enum Type { + RENDERABLE(true), + INVISIBLE(false); + + public final boolean isVisible; + + Type(boolean isVisible) { + this.isVisible = isVisible; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/PixelSource.java b/src/main/java/com/eerussianguy/blazemap/api/maps/PixelSource.java new file mode 100644 index 00000000..a3f625c8 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/PixelSource.java @@ -0,0 +1,7 @@ +package com.eerussianguy.blazemap.api.maps; + +public interface PixelSource { + int getPixel(int x, int y); + int getWidth(); + int getHeight(); +} diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/Renderable.java b/src/main/java/com/eerussianguy/blazemap/api/maps/Renderable.java new file mode 100644 index 00000000..7a8af6e3 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/Renderable.java @@ -0,0 +1,9 @@ +package com.eerussianguy.blazemap.api.maps; + +import com.mojang.blaze3d.vertex.PoseStack; + +public interface Renderable { + void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY); + int getWidth(); + int getHeight(); +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/maps/SyntheticLayer.java b/src/main/java/com/eerussianguy/blazemap/api/maps/SyntheticLayer.java new file mode 100644 index 00000000..34474e65 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/maps/SyntheticLayer.java @@ -0,0 +1,29 @@ +package com.eerussianguy.blazemap.api.maps; + +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.util.DataSource; +import com.eerussianguy.blazemap.api.util.RegionPos; +import com.mojang.blaze3d.platform.NativeImage; + +public abstract class SyntheticLayer extends Layer { + public SyntheticLayer(BlazeRegistry.Key id, TranslatableComponent name, ResourceLocation icon) { + super(id, Type.SYNTHETIC, name, icon, false); + } + + @Override // can't render + public final boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset) { + throw new UnsupportedOperationException("SyntheticLayers do not render tiles: " + getID()); + } + + @Override // can't have a legend + public final Renderable getLegendWidget() { + return null; + } + + public abstract PixelSource getPixelSource(ResourceKey dimension, RegionPos region, TileResolution resolution); +} diff --git a/src/main/java/com/eerussianguy/blazemap/api/markers/IMarkerStorage.java b/src/main/java/com/eerussianguy/blazemap/api/markers/IMarkerStorage.java deleted file mode 100644 index 369c2de1..00000000 --- a/src/main/java/com/eerussianguy/blazemap/api/markers/IMarkerStorage.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.eerussianguy.blazemap.api.markers; - -import java.util.Collection; -import java.util.Collections; - -import net.minecraft.resources.ResourceLocation; - -import com.eerussianguy.blazemap.api.BlazeRegistry; -import com.eerussianguy.blazemap.api.maps.Layer; - -public interface IMarkerStorage> { - Collection getAll(); - - void add(T marker); - - default void remove(T marker) {remove(marker.getID());} - - void remove(ResourceLocation id); - - default boolean has(T marker) {return has(marker.getID());} - - boolean has(ResourceLocation id); - - interface Layered> extends IMarkerStorage { - Collection getInLayer(BlazeRegistry.Key layerID); - - void remove(ResourceLocation id, BlazeRegistry.Key layerID); - } - - interface Dummy> extends IMarkerStorage { - @SuppressWarnings("unchecked") - default Collection getAll() {return Collections.EMPTY_LIST;} - - default void add(T marker) {} - - default void remove(T marker) {} - - default void remove(ResourceLocation id) {} - - default boolean has(T marker) {return false;} - - default boolean has(ResourceLocation id) {return false;} - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/api/markers/IStorageFactory.java b/src/main/java/com/eerussianguy/blazemap/api/markers/IStorageFactory.java deleted file mode 100644 index 40c47fe9..00000000 --- a/src/main/java/com/eerussianguy/blazemap/api/markers/IStorageFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.eerussianguy.blazemap.api.markers; - -import java.util.function.Supplier; - -import com.eerussianguy.blazemap.api.util.IOSupplier; -import com.eerussianguy.blazemap.api.util.MinecraftStreams; - -public interface IStorageFactory> { - T create(IOSupplier input, IOSupplier output, Supplier exists); -} diff --git a/src/main/java/com/eerussianguy/blazemap/api/markers/MapLabel.java b/src/main/java/com/eerussianguy/blazemap/api/markers/MapLabel.java index 3894206a..4d11610d 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/markers/MapLabel.java +++ b/src/main/java/com/eerussianguy/blazemap/api/markers/MapLabel.java @@ -1,6 +1,5 @@ package com.eerussianguy.blazemap.api.markers; -import java.util.Collections; import java.util.Set; import net.minecraft.core.BlockPos; @@ -8,72 +7,23 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; -import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; -import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.NamedMapComponent; public class MapLabel extends Marker { - private final Key layerID; - private final Set tags; + private final Key> componentId; - private Key> renderer = BlazeMapReferences.ObjectRenderers.DEFAULT; - private int width, height; - private boolean usesZoom; - - public MapLabel(ResourceLocation id, ResourceKey dimension, BlockPos position, Key layerID) { - this(id, dimension, position, layerID, BlazeMapReferences.Icons.WAYPOINT, 32, 32, -1, 0, true, Collections.EMPTY_SET); - } - - public MapLabel(ResourceLocation id, ResourceKey dimension, BlockPos position, Key layerID, ResourceLocation icon, int width, int height) { - this(id, dimension, position, layerID, icon, width, height, -1, 0, true, Collections.EMPTY_SET); - } - - public MapLabel(ResourceLocation id, ResourceKey dimension, BlockPos position, Key layerID, ResourceLocation icon, int width, int height, int color, float rotation, boolean usesZoom, Set tags) { - super(id, dimension, position, icon, color, rotation); - this.layerID = layerID; - this.width = width; - this.height = height; - this.usesZoom = usesZoom; - this.tags = tags; - } - - public final Key getLayerID() { - return layerID; - } - - public final Set getTags() { - return tags; - } - - public Key> getRenderer() { - return renderer; - } - - public MapLabel setRenderer(Key> renderer) { - this.renderer = renderer; - return this; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public MapLabel setSize(int width, int height) { - this.width = width; - this.height = height; - return this; + public MapLabel(ResourceLocation id, ResourceKey dimension, BlockPos position, ResourceLocation icon, Key> componentId) { + super(id, dimension, position, icon); + this.componentId = componentId; } - public boolean getUsesZoom() { - return usesZoom; + public MapLabel(ResourceLocation id, ResourceKey dimension, BlockPos position, ResourceLocation icon, Key> componentId, Set tags) { + super(id, dimension, position, icon, tags); + this.componentId = componentId; } - public MapLabel setUsesZoom(boolean usesZoom) { - this.usesZoom = usesZoom; - return this; + public final Key> getComponentId() { + return componentId; } -} +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/markers/Marker.java b/src/main/java/com/eerussianguy/blazemap/api/markers/Marker.java index 205bd5e0..a012e413 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/markers/Marker.java +++ b/src/main/java/com/eerussianguy/blazemap/api/markers/Marker.java @@ -1,31 +1,42 @@ package com.eerussianguy.blazemap.api.markers; import java.awt.Color; +import java.util.Collections; +import java.util.Set; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; -public abstract class Marker> { +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.BlazeRegistry; + +public class Marker> { private final ResourceLocation id; - private final ResourceKey dimension; + private ResourceKey dimension; private BlockPos.MutableBlockPos position; private ResourceLocation icon; - private int color; - private float rotation; + private int width = 32, height = 32; + private String name = null; + private boolean nameVisible = false; + private int color = -1; + private float rotation = 0F; + private boolean usesZoom = false; + private final Set tags; + private BlazeRegistry.Key> renderer = BlazeMapReferences.ObjectRenderers.DEFAULT; - protected Marker(ResourceLocation id, ResourceKey dimension, BlockPos position, ResourceLocation icon) { - this(id, dimension, position, icon, -1, 0); + @SuppressWarnings("unchecked") + public Marker(ResourceLocation id, ResourceKey dimension, BlockPos position, ResourceLocation icon) { + this(id, dimension, position, icon, (Set) Collections.EMPTY_SET); } - protected Marker(ResourceLocation id, ResourceKey dimension, BlockPos position, ResourceLocation icon, int color, float rotation) { + public Marker(ResourceLocation id, ResourceKey dimension, BlockPos position, ResourceLocation icon, Set tags) { this.id = id; this.dimension = dimension; this.position = new BlockPos.MutableBlockPos().set(position); this.icon = icon; - this.color = color; - this.rotation = rotation; + this.tags = tags; } public final ResourceLocation getID() { @@ -45,15 +56,36 @@ public BlockPos getPosition() { * You most likely won't need this. * Maybe see setPosition() instead? */ + @SuppressWarnings("unchecked") public T setPositionObject(BlockPos.MutableBlockPos position) { this.position = position; return (T) this; } + public BlazeRegistry.Key> getRenderer() { + return renderer; + } + public ResourceLocation getIcon() { return icon; } + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public String getName() { + return name; + } + + public boolean isNameVisible() { + return nameVisible; + } + public int getColor() { return color; } @@ -62,18 +94,61 @@ public float getRotation() { return rotation; } + public boolean getUsesZoom() { + return usesZoom; + } + + public final Set getTags() { + return tags; + } + + @SuppressWarnings("unchecked") + public T setDimension(ResourceKey dimension) { + this.dimension = dimension; + return (T) this; + } + @SuppressWarnings("unchecked") public T setPosition(BlockPos position) { this.position.set(position); return (T) this; } + @SuppressWarnings("unchecked") + public T setRenderer(BlazeRegistry.Key> renderer) { + this.renderer = renderer; + return (T) this; + } + @SuppressWarnings("unchecked") public T setIcon(ResourceLocation icon) { this.icon = icon; return (T) this; } + @SuppressWarnings("unchecked") + public T setSize(int width, int height) { + this.width = width; + this.height = height; + return (T) this; + } + + public T setSize(int size) { + return setSize(size, size); + } + + @SuppressWarnings("unchecked") + public T setName(String name) { + this.name = name; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T setNameVisible(boolean visible) { + this.nameVisible = visible; + return (T) this; + } + @SuppressWarnings("unchecked") public T setColor(int color) { this.color = color; @@ -86,6 +161,12 @@ public T setRotation(float rotation) { return (T) this; } + @SuppressWarnings("unchecked") + public T setUsesZoom(boolean usesZoom) { + this.usesZoom = usesZoom; + return (T) this; + } + @SuppressWarnings("unchecked") public T randomizeColor() { float hue = ((float) System.nanoTime() % 360) / 360F; diff --git a/src/main/java/com/eerussianguy/blazemap/api/markers/MarkerStorage.java b/src/main/java/com/eerussianguy/blazemap/api/markers/MarkerStorage.java new file mode 100644 index 00000000..b1d5a4de --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/markers/MarkerStorage.java @@ -0,0 +1,42 @@ +package com.eerussianguy.blazemap.api.markers; + +import java.util.Collection; +import java.util.Collections; + +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.maps.NamedMapComponent; + +public interface MarkerStorage> { + Collection getAll(); + + void add(T marker); + + default void remove(T marker) { + remove(marker.getID()); + } + + void remove(ResourceLocation id); + + default boolean has(T marker) { + return has(marker.getID()); + } + + boolean has(ResourceLocation id); + + class Dummy> implements MarkerStorage { + @SuppressWarnings("unchecked") + public Collection getAll() {return Collections.EMPTY_LIST;} + public void add(T marker) {} + public void remove(T marker) {} + public void remove(ResourceLocation id) {} + public boolean has(T marker) { return false; } + public boolean has(ResourceLocation id) { return false; } + } + + interface MapComponentStorage { + MarkerStorage getGlobal(); + MarkerStorage getStorage(BlazeRegistry.Key> componentID); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/api/markers/SearchTargeting.java b/src/main/java/com/eerussianguy/blazemap/api/markers/SearchTargeting.java index c7b03ad0..d30cc938 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/markers/SearchTargeting.java +++ b/src/main/java/com/eerussianguy/blazemap/api/markers/SearchTargeting.java @@ -4,29 +4,21 @@ * Determines if our object is the target of a search. */ public enum SearchTargeting { - /** - * No search active - */ + /** No search active */ NONE, - /** - * Search active, and our object is a result - */ + /** Search active, and our object is a result */ HIT, - /** - * Search active, and our object is not a result - */ + /** Search active, and our object is not a result */ MISS; - /** - * Helper function to override the render color in fuction of search status - */ + /** Helper to override the render color in function of search status */ public int color(int color) { return this == MISS ? 0x80808080 : color; } public int color() { - return color(-1); + return color(-1); // -1 == all bits on == pure white with full opacity } -} +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/markers/Waypoint.java b/src/main/java/com/eerussianguy/blazemap/api/markers/Waypoint.java index c12c9a78..1ee1d3ec 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/markers/Waypoint.java +++ b/src/main/java/com/eerussianguy/blazemap/api/markers/Waypoint.java @@ -7,34 +7,21 @@ import com.eerussianguy.blazemap.api.BlazeMapReferences; -public class Waypoint extends Marker { - private String name; - +public final class Waypoint extends Marker { public Waypoint(ResourceLocation id, ResourceKey dimension, BlockPos position, String name) { - this(id, dimension, position, name, BlazeMapReferences.Icons.WAYPOINT, -1, 0); + this(id, dimension, position, name, BlazeMapReferences.Icons.WAYPOINT, -1); this.randomizeColor(); } public Waypoint(ResourceLocation id, ResourceKey dimension, BlockPos position, String name, ResourceLocation icon) { - this(id, dimension, position, name, icon, -1, 0); + this(id, dimension, position, name, icon, -1); this.randomizeColor(); } public Waypoint(ResourceLocation id, ResourceKey dimension, BlockPos position, String name, ResourceLocation icon, int color) { - this(id, dimension, position, name, icon, color, 0); - } - - public Waypoint(ResourceLocation id, ResourceKey dimension, BlockPos position, String name, ResourceLocation icon, int color, float rotation) { - super(id, dimension, position, icon, color, rotation); - this.name = name; - } - - public String getName() { - return name; - } - - public Waypoint setName(String name) { - this.name = name; - return this; + super(id, dimension, position, icon); + setName(name); + setColor(color); + setNameVisible(true); } } diff --git a/src/main/java/com/eerussianguy/blazemap/api/pipeline/Collector.java b/src/main/java/com/eerussianguy/blazemap/api/pipeline/Collector.java index 2749e671..27ab7a92 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/pipeline/Collector.java +++ b/src/main/java/com/eerussianguy/blazemap/api/pipeline/Collector.java @@ -5,9 +5,13 @@ import net.minecraft.tags.FluidTags; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.material.Fluids; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.BlazeRegistry.RegistryEntry; +import com.eerussianguy.blazemap.lib.Transparency; +import com.eerussianguy.blazemap.lib.Transparency.CompositionState; /** * Collectors collect MasterData from chunks that need updating to be processed later. @@ -38,13 +42,99 @@ public Key> getOutputID() { return output; } + protected static boolean isSolid(Level level, int x, int y, int z) { + BlockState state = level.getBlockState(POS.set(x, y, z)); + return isSolid(level, state); + } + protected static boolean isSolid(Level level, BlockState state) { + CompositionState composition = Transparency.getBlockComposition(state, level, POS).getBlockCompositionState(); + return !state.getMaterial().isReplaceable() && (composition == CompositionState.BLOCK || composition == CompositionState.FLUIDLOGGED_BLOCK); + } + protected static boolean isWater(Level level, int x, int y, int z) { BlockState state = level.getBlockState(POS.set(x, y, z)); + return isWater(state); + } + protected static boolean isWater(BlockState state) { return state.getFluidState().is(FluidTags.WATER); } + protected static boolean isFluid(Level level, int x, int y, int z) { + BlockState state = level.getBlockState(POS.set(x, y, z)); + return isFluid(state); + } + protected static boolean isFluid(BlockState state) { + return !state.getFluidState().isEmpty(); + } + protected static boolean isLeavesOrReplaceable(Level level, int x, int y, int z) { BlockState state = level.getBlockState(POS.set(x, y, z)); - return state.is(BlockTags.LEAVES) || state.isAir() || state.getMaterial().isReplaceable(); + return isLeavesOrReplaceable(state); + } + protected static boolean isLeavesOrReplaceable(BlockState state) { + // TODO: For 1.20, re-add: state.getBlock() instanceof MangroveRootsBlock || state.canBeReplaced() + return state.isAir() || state.is(BlockTags.LEAVES) || state.getMaterial().isReplaceable() || state.canBeReplaced(Fluids.WATER) ; + } + + protected static boolean isSkippableAfterLeaves(Level level, int x, int y, int z) { + BlockState state = level.getBlockState(POS.set(x, y, z)); + return isSkippableAfterLeaves(state); + } + protected static boolean isSkippableAfterLeaves(BlockState state) { + return state.is(BlockTags.LOGS) || isLeavesOrReplaceable(state); + } + + protected static int findSurfaceBelowVegetation(Level level, int x, int z, boolean stopAtFluid) { + final int minBuildHeight = level.getMinBuildHeight(); + + int height = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1; + BlockState state = level.getBlockState(POS.set(x, height, z)); + + boolean foundLeaves = false; + + if (stopAtFluid) { + while(isLeavesOrReplaceable(state) && !isFluid(state)) { + height--; + if(height <= minBuildHeight) break; + state = level.getBlockState(POS.set(x, height, z)); + + foundLeaves = true; + } + + while(foundLeaves && isSkippableAfterLeaves(state) && !isFluid(state)) { + height--; + if(height <= minBuildHeight) break; + state = level.getBlockState(POS.set(x, height, z)); + } + + while (!isSolid(level, state) && !isFluid(state)) { + height--; + if(height <= minBuildHeight) break; + state = level.getBlockState(POS.set(x, height, z)); + } + + } else { + while(isLeavesOrReplaceable(state)) { + height--; + if(height <= minBuildHeight) break; + state = level.getBlockState(POS.set(x, height, z)); + + foundLeaves = true; + } + + while(foundLeaves && isSkippableAfterLeaves(state)) { + height--; + if(height <= minBuildHeight) break; + state = level.getBlockState(POS.set(x, height, z)); + } + + while (!isSolid(level, state)) { + height--; + if(height <= minBuildHeight) break; + state = level.getBlockState(POS.set(x, height, z)); + } + } + + return height; } } diff --git a/src/main/java/com/eerussianguy/blazemap/api/pipeline/Processor.java b/src/main/java/com/eerussianguy/blazemap/api/pipeline/Processor.java index 55b5f71a..fbf28789 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/pipeline/Processor.java +++ b/src/main/java/com/eerussianguy/blazemap/api/pipeline/Processor.java @@ -11,7 +11,7 @@ import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.BlazeRegistry.RegistryEntry; -import com.eerussianguy.blazemap.api.util.IDataSource; +import com.eerussianguy.blazemap.api.util.DataSource; import com.eerussianguy.blazemap.api.util.RegionPos; /** @@ -51,10 +51,10 @@ public Set>> getInputIDs() { } /** Called for ExecutionMode.DIRECT, more performant */ - public abstract void execute(ResourceKey dimension, RegionPos region, ChunkPos chunk, IDataSource data); + public abstract void execute(ResourceKey dimension, RegionPos region, ChunkPos chunk, DataSource data); /** Called for ExecutionMode.DIFFERENTIAL, more powerful */ - public abstract void execute(ResourceKey dimension, RegionPos region, ChunkPos chunk, IDataSource current, IDataSource old); + public abstract void execute(ResourceKey dimension, RegionPos region, ChunkPos chunk, DataSource current, DataSource old); /** @@ -69,7 +69,7 @@ public Direct(Key id, Key>... inputs) { } @Override - public final void execute(ResourceKey dimension, RegionPos region, ChunkPos chunk, IDataSource current, IDataSource old) { + public final void execute(ResourceKey dimension, RegionPos region, ChunkPos chunk, DataSource current, DataSource old) { throw new RuntimeException("Cannot execute differential mode on direct processor"); } } @@ -88,7 +88,7 @@ public Differential(Key id, Key>... inputs) { } @Override - public final void execute(ResourceKey dimension, RegionPos region, ChunkPos chunk, IDataSource data) { + public final void execute(ResourceKey dimension, RegionPos region, ChunkPos chunk, DataSource data) { throw new RuntimeException("Cannot execute direct mode on differential processor"); } } diff --git a/src/main/java/com/eerussianguy/blazemap/api/pipeline/Transformer.java b/src/main/java/com/eerussianguy/blazemap/api/pipeline/Transformer.java index 36178b7b..ad3cddb2 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/pipeline/Transformer.java +++ b/src/main/java/com/eerussianguy/blazemap/api/pipeline/Transformer.java @@ -6,7 +6,7 @@ import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.BlazeRegistry.RegistryEntry; -import com.eerussianguy.blazemap.api.util.IDataSource; +import com.eerussianguy.blazemap.api.util.DataSource; /** * Transformers can produce one MasterDatum by reading other MasterData, without needing access to the world. @@ -48,5 +48,5 @@ public Set>> getInputIDs() { return inputs; } - public abstract T transform(IDataSource data); + public abstract T transform(DataSource data); } diff --git a/src/main/java/com/eerussianguy/blazemap/api/util/IDataSource.java b/src/main/java/com/eerussianguy/blazemap/api/util/DataSource.java similarity index 89% rename from src/main/java/com/eerussianguy/blazemap/api/util/IDataSource.java rename to src/main/java/com/eerussianguy/blazemap/api/util/DataSource.java index 2102897b..c03b51ab 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/util/IDataSource.java +++ b/src/main/java/com/eerussianguy/blazemap/api/util/DataSource.java @@ -4,6 +4,6 @@ import com.eerussianguy.blazemap.api.pipeline.DataType; import com.eerussianguy.blazemap.api.pipeline.MasterDatum; -public interface IDataSource { +public interface DataSource { T get(Key> key); } diff --git a/src/main/java/com/eerussianguy/blazemap/api/util/IOSupplier.java b/src/main/java/com/eerussianguy/blazemap/api/util/IOSupplier.java deleted file mode 100644 index f7b68d7a..00000000 --- a/src/main/java/com/eerussianguy/blazemap/api/util/IOSupplier.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.eerussianguy.blazemap.api.util; - -import java.io.IOException; - -@FunctionalInterface -public interface IOSupplier { - T get() throws IOException; -} diff --git a/src/main/java/com/eerussianguy/blazemap/api/util/IStorageAccess.java b/src/main/java/com/eerussianguy/blazemap/api/util/IStorageAccess.java deleted file mode 100644 index e343b57b..00000000 --- a/src/main/java/com/eerussianguy/blazemap/api/util/IStorageAccess.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.eerussianguy.blazemap.api.util; - -import java.io.IOException; - -import net.minecraft.resources.ResourceLocation; - -public interface IStorageAccess { - boolean exists(ResourceLocation node); - boolean exists(ResourceLocation node, String child); - - MinecraftStreams.Input read(ResourceLocation node) throws IOException; - MinecraftStreams.Input read(ResourceLocation node, String child) throws IOException; - - MinecraftStreams.Output write(ResourceLocation node) throws IOException; - MinecraftStreams.Output write(ResourceLocation node, String child) throws IOException; - - void move(ResourceLocation source, ResourceLocation destination) throws IOException; - void move(ResourceLocation node, String source, String destination) throws IOException; -} diff --git a/src/main/java/com/eerussianguy/blazemap/api/util/MinecraftStreams.java b/src/main/java/com/eerussianguy/blazemap/api/util/MinecraftStreams.java index 76d5aa6b..6276920d 100644 --- a/src/main/java/com/eerussianguy/blazemap/api/util/MinecraftStreams.java +++ b/src/main/java/com/eerussianguy/blazemap/api/util/MinecraftStreams.java @@ -1,12 +1,11 @@ package com.eerussianguy.blazemap.api.util; import java.io.*; +import java.util.Collection; import net.minecraft.core.BlockPos; -import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; import com.eerussianguy.blazemap.api.BlazeRegistry; @@ -16,13 +15,15 @@ public Output(OutputStream out) { super(out); } - public void writeResourceLocation(ResourceLocation resourceLocation) throws IOException { - writeUTF(resourceLocation.toString()); + public void writeCollection(Collection collection, IOConsumer consumer) throws IOException { + writeInt(collection.size()); + for(T element : collection) { + consumer.accept(element); + } } - public void writeDimensionKey(ResourceKey dimension) throws IOException { - writeResourceLocation(dimension.registry()); - writeResourceLocation(dimension.location()); + public void writeResourceLocation(ResourceLocation resourceLocation) throws IOException { + writeUTF(resourceLocation.toString()); } public void writeKey(BlazeRegistry.Key key) throws IOException { @@ -48,14 +49,15 @@ public Input(InputStream in) { super(in); } - public ResourceLocation readResourceLocation() throws IOException { - return new ResourceLocation(readUTF()); + public void readCollection(IORunnable function) throws IOException { + int count = readInt(); + for(int i = 0; i < count; i++) { + function.run(); + } } - public ResourceKey readDimensionKey() throws IOException { - ResourceLocation registry = readResourceLocation(); - ResourceLocation location = readResourceLocation(); - return ResourceKey.create(ResourceKey.createRegistryKey(registry), location); + public ResourceLocation readResourceLocation() throws IOException { + return new ResourceLocation(readUTF()); } public BlazeRegistry.Key readKey(BlazeRegistry registry) throws IOException { @@ -77,4 +79,14 @@ public RegionPos readRegionPos() throws IOException { return new RegionPos(x, z); } } -} + + @FunctionalInterface + public interface IORunnable { + void run() throws IOException; + } + + @FunctionalInterface + public interface IOConsumer { + void accept(T t) throws IOException; + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/api/util/StorageAccess.java b/src/main/java/com/eerussianguy/blazemap/api/util/StorageAccess.java new file mode 100644 index 00000000..804d314f --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/api/util/StorageAccess.java @@ -0,0 +1,52 @@ +package com.eerussianguy.blazemap.api.util; + +import java.io.IOException; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import net.minecraft.resources.ResourceLocation; + +/** + * Provides access to centralized local storage. + * Addons can use this to store their data along with the Blaze Map data. + * + * @author LordFokas + */ +public interface StorageAccess { + boolean exists(ResourceLocation node); + boolean exists(ResourceLocation node, String child); + + MinecraftStreams.Input read(ResourceLocation node) throws IOException; + MinecraftStreams.Input read(ResourceLocation node, String child) throws IOException; + + MinecraftStreams.Output write(ResourceLocation node) throws IOException; + MinecraftStreams.Output write(ResourceLocation node, String child) throws IOException; + + void move(ResourceLocation source, ResourceLocation destination) throws IOException; + void move(ResourceLocation node, String source, String destination) throws IOException; + + /** Per-level storage. */ + interface LevelStorage extends StorageAccess { } + + /** + * Per-server storage. + * On the dedicated server this is, for all purposes, global. + * On the physical client these can be in 3 types / locations: + * - server-sided storage attached to the save, for the integrated server + * - client-sided storage attached to the save, for playing in local worlds + * - client-sided storage stored in a different path, for playing in remote servers + */ + interface ServerStorage extends StorageAccess { + LevelStorage getLevelStorage(ResourceLocation dimension); + void forEachLevel(BiConsumer consumer); + } + + /** + * For storing global stuff. + * Only exists in the physical client because for the dedicated server the ServerStorage is already global. + */ + interface GlobalStorage extends StorageAccess { + void foreachSave(Consumer save); + void foreachServer(Consumer server); + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/config/BlazeMapConfig.java b/src/main/java/com/eerussianguy/blazemap/config/BlazeMapConfig.java index fcb354af..0bda85d7 100644 --- a/src/main/java/com/eerussianguy/blazemap/config/BlazeMapConfig.java +++ b/src/main/java/com/eerussianguy/blazemap/config/BlazeMapConfig.java @@ -10,6 +10,7 @@ public class BlazeMapConfig { public static final ClientConfig CLIENT = register(ClientConfig::new, ModConfig.Type.CLIENT); public static final CommonConfig COMMON = register(CommonConfig::new, ModConfig.Type.COMMON); + public static final ServerConfig SERVER = register(ServerConfig::new, ModConfig.Type.SERVER); public static void init() {} diff --git a/src/main/java/com/eerussianguy/blazemap/config/ClientConfig.java b/src/main/java/com/eerussianguy/blazemap/config/ClientConfig.java index b6596b50..91f689bc 100644 --- a/src/main/java/com/eerussianguy/blazemap/config/ClientConfig.java +++ b/src/main/java/com/eerussianguy/blazemap/config/ClientConfig.java @@ -7,15 +7,17 @@ import net.minecraftforge.fml.loading.FMLEnvironment; import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapAPI; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.Overlay; import com.eerussianguy.blazemap.feature.maps.MinimapRenderer; import com.eerussianguy.blazemap.feature.maps.WorldMapGui; -import com.eerussianguy.blazemap.util.IConfigAdapter; -import com.eerussianguy.blazemap.util.LayerListAdapter; -import com.eerussianguy.blazemap.util.MapTypeAdapter; +import com.eerussianguy.blazemap.config.adapter.ConfigAdapter; +import com.eerussianguy.blazemap.config.adapter.NamedMapComponentListAdapter; +import com.eerussianguy.blazemap.config.adapter.MapTypeAdapter; /** * Forge configs happen to be a very simple way to serialize things across saves and hold data within a particular instance @@ -58,6 +60,7 @@ public static class FeaturesConfig { public final BooleanValue displayOtherPlayers; public final BooleanValue displayWaypointsOnMap; public final BooleanValue renderWaypointsInWorld; + public final BooleanValue deathWaypoints; FeaturesConfig(Function builder) { this.displayCoords = builder.apply("displayCoords") @@ -80,21 +83,27 @@ public static class FeaturesConfig { this.renderWaypointsInWorld = builder.apply("renderWaypointsInWorld") .comment("Enables waypoints to be rendered in the world") .define("renderWaypointsInWorld", false); + this.deathWaypoints = builder.apply("deathWaypoints") + .comment("Automatically create special waypoints at the place of your death") + .define("deathWaypoints", true); } } public static class MapConfig { - public final IConfigAdapter> activeMap; - public final IConfigAdapter>> disabledLayers; + public final ConfigAdapter> activeMap; + public final ConfigAdapter>> disabledLayers; + public final ConfigAdapter>> disabledOverlays; public final DoubleValue zoom; MapConfig(Function builder, double minZoom, double maxZoom) { ConfigValue _activeMap = builder.apply("activeMap").comment("List of disabled Layers, comma separated").define("activeMap", BlazeMapReferences.MapTypes.AERIAL_VIEW.toString()); ConfigValue> _disabledLayers = builder.apply("disabledLayers").comment("List of disabled Layers, comma separated").defineList("disabledLayers", List::of, o -> o instanceof String); + ConfigValue> _disabledOverlays = builder.apply("disabledOverlays").comment("List of disabled Overlays, comma separated").defineList("disabledOverlays", List::of, o -> o instanceof String); this.zoom = builder.apply("zoom").comment("Zoom level. Must be a power of 2").defineInRange("zoom", 1.0, minZoom, maxZoom); this.activeMap = new MapTypeAdapter(_activeMap); - this.disabledLayers = new LayerListAdapter(_disabledLayers); + this.disabledLayers = new NamedMapComponentListAdapter<>(_disabledLayers, BlazeMapAPI.LAYERS); + this.disabledOverlays = new NamedMapComponentListAdapter<>(_disabledOverlays, BlazeMapAPI.OVERLAYS); } } diff --git a/src/main/java/com/eerussianguy/blazemap/config/ServerConfig.java b/src/main/java/com/eerussianguy/blazemap/config/ServerConfig.java new file mode 100644 index 00000000..998c408b --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/config/ServerConfig.java @@ -0,0 +1,146 @@ +package com.eerussianguy.blazemap.config; + +import java.util.HashMap; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.ForgeConfigSpec.*; +import net.minecraftforge.registries.ForgeRegistries; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapAPI; +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.NamedMapComponent; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.config.adapter.ConfigAdapter; +import com.eerussianguy.blazemap.config.adapter.NamedMapComponentListAdapter; + +public class ServerConfig { + public final NamedMapComponentPermissions mapPermissions; + public final NamedMapComponentPermissions layerPermissions; + public final NamedMapComponentPermissions overlayPermissions; + public final MapItemRequirement mapItemRequirement; + + ServerConfig(Builder innerBuilder) { + Function builder = name -> innerBuilder.translation(BlazeMap.MOD_ID + ".config.server." + name); + + innerBuilder.push("permissions"); + + innerBuilder.comment("Restrict which maps the players may use"); + innerBuilder.push("maps"); + mapPermissions = new NamedMapComponentPermissions<>(builder, "maps", BlazeMapAPI.MAPTYPES); + innerBuilder.pop(); + + innerBuilder.comment("Restrict which layers the players may use"); + innerBuilder.push("layers"); + layerPermissions = new NamedMapComponentPermissions<>(builder, "layers", BlazeMapAPI.LAYERS); + innerBuilder.pop(); + + innerBuilder.comment("Restrict which overlays the players may use"); + innerBuilder.push("overlays"); + overlayPermissions = new NamedMapComponentPermissions<>(builder, "overlays", BlazeMapAPI.OVERLAYS); + innerBuilder.pop(); + + innerBuilder.pop(); + + innerBuilder.comment("Configurations for requiring an item to map the world"); + innerBuilder.push("required_item"); + mapItemRequirement = new MapItemRequirement(builder); + innerBuilder.pop(); + } + + public static final class NamedMapComponentPermissions> { + private final EnumValue listMode; + private final ConfigAdapter>> list; + + NamedMapComponentPermissions(Function builder, String key, BlazeRegistry registry){ + listMode = builder.apply("mode").defineEnum("mode", ListMode.BLACKLIST); + ConfigValue> _list = builder.apply(key).comment("List of "+key+", comma separated").defineList(key, List::of, o -> o instanceof String); + list = new NamedMapComponentListAdapter<>(_list, registry); + } + + public boolean isAllowed(BlazeRegistry.Key key) { + return listMode.get().allows(list.get().contains(key)); + } + } + + private enum ListMode { + WHITELIST, BLACKLIST; + + boolean allows(boolean found) { + return switch(this) { + case BLACKLIST -> !found; + case WHITELIST -> found; + }; + } + } + + public static final class MapItemRequirement { + private final EnumValue write_mode, read_mode; + private final ConfigValue item; + private final HashMap> predicates = new HashMap<>(); + + MapItemRequirement(Function builder) { + write_mode = builder.apply("write_mode").comment("Restriction level to update the player's map").defineEnum("write_mode", RequirementMode.UNRESTRICTED); + read_mode = builder.apply("read_mode") + .comment("Restriction level to show the players minimap.\nIf not unrestricted, the restriction level for the fullscreen map is always ITEM_IN_INVENTORY, for convenience.") + .defineEnum("read_mode", RequirementMode.UNRESTRICTED); + item = builder.apply("map_item").comment("Required item, if not unrestricted. Item ID or Tag. Tags must begin with '#'").define("map_item", "minecraft:map"); + } + + public boolean canPlayerAccessMap(Player player, MapAccess access) { + RequirementMode mode = (access == MapAccess.WRITE ? write_mode : read_mode).get(); + if(mode == RequirementMode.UNRESTRICTED) return true; + if(access == MapAccess.READ_STATIC) mode = RequirementMode.ITEM_IN_INVENTORY; + + Predicate predicate = predicates.computeIfAbsent(item.get(), string -> { + if(string.startsWith("#")) { + var manager = ForgeRegistries.ITEMS.tags(); + var tagKey = manager.createTagKey(new ResourceLocation(string.replace("#", ""))); + return (ItemStack stack) -> stack.is(tagKey); + } else { + var id = new ResourceLocation(string); + return (ItemStack stack) -> stack.getItem().getRegistryName().equals(id); + } + }); + + var inventory = player.getInventory(); + switch(mode) { + case ITEM_IN_INVENTORY: + for(int slot = 9; slot < 36; slot++) { + if(predicate.test(inventory.getItem(slot))) return true; + } + case ITEM_IN_HOTBAR: + for(int slot = 0; slot < 9; slot++) { + if(predicate.test(inventory.getItem(slot))) return true; + } + case ITEM_IN_HAND: + return predicate.test(player.getItemInHand(InteractionHand.MAIN_HAND)) + || predicate.test(player.getItemInHand(InteractionHand.OFF_HAND)); + } + + BlazeMap.LOGGER.error("Execution should never be able to reach this point: Unable to calculate player mapping restriction"); + return false; + } + } + + public enum MapAccess { + WRITE, // Update the map + READ_LIVE, // Read the map live, like with minimaps + READ_STATIC // Read the map in a more static context, like stopping to open the full map + } + + private enum RequirementMode { + UNRESTRICTED, + ITEM_IN_INVENTORY, + ITEM_IN_HOTBAR, + ITEM_IN_HAND + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/config/adapter/ConfigAdapter.java b/src/main/java/com/eerussianguy/blazemap/config/adapter/ConfigAdapter.java new file mode 100644 index 00000000..454d1dc1 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/config/adapter/ConfigAdapter.java @@ -0,0 +1,7 @@ +package com.eerussianguy.blazemap.config.adapter; + +public interface ConfigAdapter { + T get(); + + void set(T value); +} diff --git a/src/main/java/com/eerussianguy/blazemap/util/MapTypeAdapter.java b/src/main/java/com/eerussianguy/blazemap/config/adapter/MapTypeAdapter.java similarity index 82% rename from src/main/java/com/eerussianguy/blazemap/util/MapTypeAdapter.java rename to src/main/java/com/eerussianguy/blazemap/config/adapter/MapTypeAdapter.java index 03bdc0cb..821935ed 100644 --- a/src/main/java/com/eerussianguy/blazemap/util/MapTypeAdapter.java +++ b/src/main/java/com/eerussianguy/blazemap/config/adapter/MapTypeAdapter.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.util; +package com.eerussianguy.blazemap.config.adapter; import net.minecraftforge.common.ForgeConfigSpec; @@ -6,7 +6,7 @@ import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.maps.MapType; -public class MapTypeAdapter implements IConfigAdapter> { +public class MapTypeAdapter implements ConfigAdapter> { private final ForgeConfigSpec.ConfigValue target; public MapTypeAdapter(ForgeConfigSpec.ConfigValue target) { diff --git a/src/main/java/com/eerussianguy/blazemap/config/adapter/NamedMapComponentListAdapter.java b/src/main/java/com/eerussianguy/blazemap/config/adapter/NamedMapComponentListAdapter.java new file mode 100644 index 00000000..e01ba40d --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/config/adapter/NamedMapComponentListAdapter.java @@ -0,0 +1,32 @@ +package com.eerussianguy.blazemap.config.adapter; + +import java.util.List; +import java.util.stream.Collectors; + +import net.minecraftforge.common.ForgeConfigSpec; + +import com.eerussianguy.blazemap.api.BlazeMapAPI; +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.BlazeRegistry.Key; +import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.NamedMapComponent; + +public class NamedMapComponentListAdapter> implements ConfigAdapter>> { + private final ForgeConfigSpec.ConfigValue> target; + private final BlazeRegistry registry; + + public NamedMapComponentListAdapter(ForgeConfigSpec.ConfigValue> target, BlazeRegistry registry) { + this.target = target; + this.registry = registry; + } + + @Override + public List> get() { + return target.get().stream().map(registry::findOrCreate).collect(Collectors.toList()); + } + + @Override + public void set(List> value) { + target.set(value.stream().map(Key::toString).collect(Collectors.toList())); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/engine/BlazeMapAsync.java b/src/main/java/com/eerussianguy/blazemap/engine/BlazeMapAsync.java index d74bd2a3..e0f47aa8 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/BlazeMapAsync.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/BlazeMapAsync.java @@ -1,10 +1,11 @@ package com.eerussianguy.blazemap.engine; -import com.eerussianguy.blazemap.engine.async.AsyncChainRoot; -import com.eerussianguy.blazemap.engine.async.AsyncDataCruncher; -import com.eerussianguy.blazemap.engine.async.DebouncingThread; -import com.eerussianguy.blazemap.engine.server.BlazeMapServerEngine; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.engine.server.ServerEngine; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.async.AsyncChainRoot; +import com.eerussianguy.blazemap.lib.async.AsyncDataCruncher; +import com.eerussianguy.blazemap.lib.async.DebouncingThread; public class BlazeMapAsync { private static BlazeMapAsync instance; @@ -21,9 +22,9 @@ public static BlazeMapAsync instance() { } private BlazeMapAsync() { - cruncher = new AsyncDataCruncher("Blaze Map"); - serverChain = new AsyncChainRoot(cruncher, BlazeMapServerEngine::submit); + cruncher = new AsyncDataCruncher("Blaze Map", BlazeMap.LOGGER); + serverChain = new AsyncChainRoot(cruncher, ServerEngine::submit); clientChain = new AsyncChainRoot(cruncher, Helpers::runOnMainThread); - debouncer = new DebouncingThread("Blaze Map"); + debouncer = new DebouncingThread("Blaze Map", BlazeMap.LOGGER); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/MDSources.java b/src/main/java/com/eerussianguy/blazemap/engine/MDSources.java similarity index 87% rename from src/main/java/com/eerussianguy/blazemap/feature/MDSources.java rename to src/main/java/com/eerussianguy/blazemap/engine/MDSources.java index 1fbd531a..6a693db8 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/MDSources.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/MDSources.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.feature; +package com.eerussianguy.blazemap.engine; public class MDSources { public static class Client { diff --git a/src/main/java/com/eerussianguy/blazemap/engine/Pipeline.java b/src/main/java/com/eerussianguy/blazemap/engine/Pipeline.java index 8c249353..74dedf09 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/Pipeline.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/Pipeline.java @@ -7,16 +7,19 @@ import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.pipeline.*; import com.eerussianguy.blazemap.api.util.RegionPos; -import com.eerussianguy.blazemap.engine.async.AsyncChainRoot; -import com.eerussianguy.blazemap.engine.async.DebouncingDomain; -import com.eerussianguy.blazemap.engine.async.DebouncingThread; import com.eerussianguy.blazemap.engine.cache.ChunkMDCache; import com.eerussianguy.blazemap.engine.cache.ChunkMDCacheView; import com.eerussianguy.blazemap.engine.cache.LevelMDCache; +import com.eerussianguy.blazemap.engine.storage.InternalStorage; +import com.eerussianguy.blazemap.engine.storage.PublicStorage; +import com.eerussianguy.blazemap.lib.async.AsyncChainRoot; +import com.eerussianguy.blazemap.lib.async.DebouncingDomain; +import com.eerussianguy.blazemap.lib.async.DebouncingThread; import static com.eerussianguy.blazemap.engine.UnsafeGenerics.*; @@ -43,21 +46,21 @@ public abstract class Pipeline { private final List transformers; private final List processors; public final int numCollectors, numProcessors, numTransformers; - protected final StorageAccess.Internal storage; - public final StorageAccess addonStorage; + protected final InternalStorage storage; + public final PublicStorage addonStorage; protected final LevelMDCache mdCache; private boolean useMDCache = false; protected Pipeline( AsyncChainRoot async, DebouncingThread debouncer, PipelineProfiler profiler, - ResourceKey dimension, Supplier level, StorageAccess.Internal storage, + ResourceKey dimension, Supplier level, InternalStorage storage, Set>> availableCollectors, Set>> availableTransformers, Set> availableProcessors ) { this.async = async; - this.dirtyChunks = new DebouncingDomain<>(debouncer, this::begin, 500, 5000); + this.dirtyChunks = new DebouncingDomain<>(debouncer, this::begin, 500, 5000, BlazeMap.LOGGER); this.profiler = profiler; this.dimension = dimension; @@ -107,7 +110,7 @@ public void insertMasterData(ChunkPos pos, List data) { // ================================================================================================================= // Pipeline internals - private void begin(ChunkPos pos) { + protected void begin(ChunkPos pos) { async.startOnGameThread($ -> this.runCollectors(pos)) .thenOnDataThread(data -> this.processMasterData(pos, data)) .execute(); diff --git a/src/main/java/com/eerussianguy/blazemap/engine/RegistryController.java b/src/main/java/com/eerussianguy/blazemap/engine/RegistryController.java index f1cf03ad..87988373 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/RegistryController.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/RegistryController.java @@ -6,8 +6,9 @@ import net.minecraftforge.fml.loading.FMLEnvironment; import com.eerussianguy.blazemap.api.BlazeMapAPI; +import com.eerussianguy.blazemap.api.event.BlazeRegistriesFrozenEvent; import com.eerussianguy.blazemap.api.event.BlazeRegistryEvent; -import com.eerussianguy.blazemap.profiling.KnownMods; +import com.eerussianguy.blazemap.integration.KnownMods; public class RegistryController { private static boolean frozenRegistries = false; @@ -31,7 +32,13 @@ public static synchronized void ensureRegistriesReady() { ensureClientRegistriesReady(); } frozenRegistries = true; - KnownMods.init(); + + bus.post(new BlazeRegistriesFrozenEvent()); + + KnownMods.addRegistry(BlazeMapAPI.MASTER_DATA); + KnownMods.addRegistry(BlazeMapAPI.COLLECTORS); + KnownMods.addRegistry(BlazeMapAPI.TRANSFORMERS); + KnownMods.addRegistry(BlazeMapAPI.PROCESSORS); } } @@ -44,7 +51,26 @@ private static void ensureClientRegistriesReady() { bus.post(new BlazeRegistryEvent.MapTypeRegistryEvent()); BlazeMapAPI.MAPTYPES.freeze(); + bus.post(new BlazeRegistryEvent.OverlayRegistryEvent()); + BlazeMapAPI.OVERLAYS.freeze(); + bus.post(new BlazeRegistryEvent.ObjectRendererRegistryEvent()); BlazeMapAPI.OBJECT_RENDERERS.freeze(); + + for(var key : BlazeMapAPI.MAPTYPES.keys()) { + key.value().inflate(); + } + + KnownMods.addRegistry(BlazeMapAPI.LAYERS); + KnownMods.addRegistry(BlazeMapAPI.MAPTYPES); + KnownMods.addRegistry(BlazeMapAPI.OVERLAYS); + KnownMods.addRegistry(BlazeMapAPI.OBJECT_RENDERERS); + } + + public static void freezeClientRegistries() { + BlazeMapAPI.LAYERS.freeze(); + BlazeMapAPI.MAPTYPES.freeze(); + BlazeMapAPI.OVERLAYS.freeze(); + BlazeMapAPI.OBJECT_RENDERERS.freeze(); } } diff --git a/src/main/java/com/eerussianguy/blazemap/engine/StorageAccess.java b/src/main/java/com/eerussianguy/blazemap/engine/StorageAccess.java deleted file mode 100644 index 027b50a5..00000000 --- a/src/main/java/com/eerussianguy/blazemap/engine/StorageAccess.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.eerussianguy.blazemap.engine; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.Objects; -import java.util.regex.Pattern; -import java.util.regex.Matcher; - -import net.minecraft.resources.ResourceLocation; - -import com.eerussianguy.blazemap.BlazeMap; -import com.eerussianguy.blazemap.api.maps.TileResolution; -import com.eerussianguy.blazemap.api.util.IStorageAccess; -import com.eerussianguy.blazemap.api.util.MinecraftStreams; - -public class StorageAccess implements IStorageAccess { - protected final File dir; - - public StorageAccess(File dir) { - this.dir = dir; - } - - @Override - public boolean exists(ResourceLocation node) { - return getFile(node).exists(); - } - - @Override - public boolean exists(ResourceLocation node, String child) { - return getFile(node, child).exists(); - } - - @Override - public MinecraftStreams.Input read(ResourceLocation node) throws IOException { - return new MinecraftStreams.Input(new FileInputStream(getFile(node))); - } - - @Override - public MinecraftStreams.Input read(ResourceLocation node, String child) throws IOException { - return new MinecraftStreams.Input(new FileInputStream(getFile(node, child))); - } - - @Override - public MinecraftStreams.Output write(ResourceLocation node) throws IOException { - return new MinecraftStreams.Output(new FileOutputStream(getFile(node))); - } - - @Override - public MinecraftStreams.Output write(ResourceLocation node, String child) throws IOException { - return new MinecraftStreams.Output(new FileOutputStream(getFile(node, child))); - } - - @Override - public void move(ResourceLocation source, ResourceLocation destination) throws IOException { - move(getFile(source), getFile(destination)); - } - - @Override - public void move(ResourceLocation node, String source, String destination) throws IOException { - move(getFile(node, source), getFile(node, destination)); - } - - protected void move(File source, File destination) throws IOException { - Files.move(source.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - protected File getFile(ResourceLocation node) { - Objects.requireNonNull(node); - File mod = new File(dir, node.getNamespace()); - File file = new File(mod, node.getPath()); - file.getParentFile().mkdirs(); - return file; - } - - protected File getFile(ResourceLocation node, String child) { - Objects.requireNonNull(node); - File dir = getFile(node); - dir.mkdirs(); - return new File(dir, child); - } - - public static class Internal extends StorageAccess { - private static final String PATTERN = "[%s] %s"; - private static final String PATTERN_MIP = "[%s] %s [%d]"; - private static final String PATTERN_OLD = "%s+%s"; - private static final Pattern PATTERN_OLD_REGEX = Pattern.compile("^([a-z0-9_]+)\\+([a-z0-9_]+)$"); - - public Internal(File dir, String child) { - this(new File(dir, child)); - } - - public Internal(File dir) { - super(dir); - dir.mkdirs(); - } - - @Override - public File getFile(ResourceLocation node) { - Objects.requireNonNull(node); - File file = new File(dir, String.format(PATTERN, node.getNamespace(), node.getPath())); - file.getParentFile().mkdirs(); - return file; - } - - public File getMipmap(ResourceLocation node, String file, TileResolution resolution) { - Objects.requireNonNull(node); - File d = new File(dir, String.format(PATTERN_MIP, node.getNamespace(), node.getPath(), resolution.pixelWidth)); - d.mkdirs(); - return new File(d, file); - } - - /** - * Port files from their v0.4 location to their v0.5 location - */ - public void tryPortDimension(ResourceLocation node) throws IOException { - Objects.requireNonNull(node); - File newFile = getFile(node).getParentFile(); - - if ( !newFile.exists() || - (newFile.isDirectory() && newFile.list().length == 0)) - { - File oldFile = new File(dir.getParent(), String.format(PATTERN_OLD, node.getNamespace(), node.getPath())); - oldFile.getParentFile().mkdirs(); - - if (oldFile.exists()) { - move(oldFile, newFile); - - File[] layers = newFile.listFiles(); - for (File layer : layers) { - portLayer(layer); - } - } - } - } - - public void portLayer(File oldFile) throws IOException { - String oldFilename = oldFile.getName(); - Matcher matcher = PATTERN_OLD_REGEX.matcher(oldFilename); - - if (matcher.matches()) { - File newFile = new File(dir, String.format(PATTERN_MIP, matcher.group(1), matcher.group(2), TileResolution.FULL.pixelWidth)); - newFile.mkdirs(); - - move(oldFile, newFile); - } - } - - public StorageAccess addon() { - return new StorageAccess(dir); - } - - public StorageAccess addon(ResourceLocation node) { - return new StorageAccess(getFile(node)); - } - - public StorageAccess.Internal internal(ResourceLocation node) { - return new Internal(getFile(node)); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/engine/cache/ChunkMDCacheView.java b/src/main/java/com/eerussianguy/blazemap/engine/cache/ChunkMDCacheView.java index 99187f6e..f1d8f11d 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/cache/ChunkMDCacheView.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/cache/ChunkMDCacheView.java @@ -5,11 +5,11 @@ import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.pipeline.DataType; import com.eerussianguy.blazemap.api.pipeline.MasterDatum; -import com.eerussianguy.blazemap.api.util.IDataSource; +import com.eerussianguy.blazemap.api.util.DataSource; import com.eerussianguy.blazemap.engine.UnsafeGenerics; @SuppressWarnings("rawtypes") -public class ChunkMDCacheView implements IDataSource { +public class ChunkMDCacheView implements DataSource { private ChunkMDCache source; private Set> filter; diff --git a/src/main/java/com/eerussianguy/blazemap/engine/cache/LevelMDCache.java b/src/main/java/com/eerussianguy/blazemap/engine/cache/LevelMDCache.java index 7d24be46..8f69de26 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/cache/LevelMDCache.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/cache/LevelMDCache.java @@ -8,30 +8,28 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalNotification; - import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.ChunkPos; import com.eerussianguy.blazemap.BlazeMap; -import com.eerussianguy.blazemap.api.util.IStorageAccess; +import com.eerussianguy.blazemap.api.util.StorageAccess; import com.eerussianguy.blazemap.api.util.MinecraftStreams; import com.eerussianguy.blazemap.api.util.RegionPos; import com.eerussianguy.blazemap.engine.BlazeMapAsync; -import com.eerussianguy.blazemap.engine.async.AsyncChainRoot; -import com.eerussianguy.blazemap.engine.async.DebouncingDomain; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.lib.async.AsyncChainRoot; +import com.eerussianguy.blazemap.lib.async.DebouncingDomain; public class LevelMDCache { - private static final ResourceLocation NODE = Helpers.identifier("md-cache"); + private static final ResourceLocation NODE = BlazeMap.resource("md-cache"); private final LoadingCache regions; - private final IStorageAccess storage; + private final StorageAccess storage; private final DebouncingDomain debouncer; private final AsyncChainRoot asyncChain; - public LevelMDCache(final IStorageAccess storage, AsyncChainRoot asyncChain) { + public LevelMDCache(final StorageAccess storage, AsyncChainRoot asyncChain) { this.storage = storage; this.asyncChain = asyncChain; - this.debouncer = new DebouncingDomain<>(BlazeMapAsync.instance().debouncer, this::persist, 5_000, 30_000); + this.debouncer = new DebouncingDomain<>(BlazeMapAsync.instance().debouncer, this::persist, 5_000, 30_000, BlazeMap.LOGGER); this.regions = CacheBuilder.newBuilder() .maximumSize(256) @@ -54,12 +52,18 @@ public RegionMDCache load(RegionPos key) { cache.read(stream); } catch (Exception e) { + // Unlocking old cache before we discard cache + // TODO: There must be a better way of doing this? + cache.fileLock.readLock().unlock(); + // Couldn't populate the cache from the existing cache file (corruption?) // so behaving as if no cache file was found cache = new RegionMDCache(key, debouncer); } finally { - cache.fileLock.readLock().unlock(); + if (cache.fileLock.getReadHoldCount() > 0) { + cache.fileLock.readLock().unlock(); + } } } diff --git a/src/main/java/com/eerussianguy/blazemap/engine/cache/RegionMDCache.java b/src/main/java/com/eerussianguy/blazemap/engine/cache/RegionMDCache.java index c6ef9e77..e32222e6 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/cache/RegionMDCache.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/cache/RegionMDCache.java @@ -1,18 +1,17 @@ package com.eerussianguy.blazemap.engine.cache; import java.io.IOException; +import java.util.Iterator; import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.Iterator; - import net.minecraft.world.level.ChunkPos; import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.util.MinecraftStreams; import com.eerussianguy.blazemap.api.util.RegionPos; -import com.eerussianguy.blazemap.engine.async.DebouncingDomain; +import com.eerussianguy.blazemap.lib.async.DebouncingDomain; import com.eerussianguy.blazemap.profiling.Profilers; public class RegionMDCache { diff --git a/src/main/java/com/eerussianguy/blazemap/engine/client/BlazeMapClientEngine.java b/src/main/java/com/eerussianguy/blazemap/engine/client/ClientEngine.java similarity index 76% rename from src/main/java/com/eerussianguy/blazemap/engine/client/BlazeMapClientEngine.java rename to src/main/java/com/eerussianguy/blazemap/engine/client/ClientEngine.java index 47f355fa..92c120d3 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/client/BlazeMapClientEngine.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/client/ClientEngine.java @@ -1,5 +1,6 @@ package com.eerussianguy.blazemap.engine.client; +import java.io.File; import java.util.*; import java.util.function.Consumer; @@ -7,70 +8,84 @@ import net.minecraft.client.player.LocalPlayer; import net.minecraft.network.Connection; import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.LevelResource; import net.minecraftforge.client.event.ClientPlayerNetworkEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import com.eerussianguy.blazemap.BlazeMap; -import com.eerussianguy.blazemap.config.BlazeMapConfig; import com.eerussianguy.blazemap.api.event.DimensionChangedEvent; -import com.eerussianguy.blazemap.api.event.ServerJoinedEvent; +import com.eerussianguy.blazemap.api.event.ClientEngineEvent; import com.eerussianguy.blazemap.api.maps.LayerRegion; -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; -import com.eerussianguy.blazemap.api.markers.IStorageFactory; -import com.eerussianguy.blazemap.api.markers.MapLabel; -import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.api.markers.MarkerStorage; import com.eerussianguy.blazemap.api.pipeline.MasterDatum; import com.eerussianguy.blazemap.api.pipeline.PipelineType; -import com.eerussianguy.blazemap.api.util.IStorageAccess; +import com.eerussianguy.blazemap.api.util.StorageAccess; +import com.eerussianguy.blazemap.config.BlazeMapConfig; import com.eerussianguy.blazemap.engine.BlazeMapAsync; import com.eerussianguy.blazemap.engine.RegistryController; -import com.eerussianguy.blazemap.engine.StorageAccess; +import com.eerussianguy.blazemap.engine.storage.InternalStorage; import com.eerussianguy.blazemap.engine.cache.ChunkMDCache; +import com.eerussianguy.blazemap.engine.storage.PublicStorage; +import com.eerussianguy.blazemap.engine.storage.StorageType; +import com.eerussianguy.blazemap.lib.Helpers; import com.eerussianguy.blazemap.network.BlazeNetwork; -import com.eerussianguy.blazemap.util.Helpers; -public class BlazeMapClientEngine { +public class ClientEngine { private static final Set> TILE_CHANGE_LISTENERS = new HashSet<>(); private static final Map, ClientPipeline> PIPELINES = new HashMap<>(); - private static final Map, IMarkerStorage> WAYPOINTS = new HashMap<>(); - private static final ResourceLocation WAYPOINT_STORAGE = Helpers.identifier("waypoints.bin"); private static ClientPipeline activePipeline; - private static IMarkerStorage.Layered activeLabels; - private static IMarkerStorage activeWaypoints; - private static IStorageFactory> waypointStorageFactory; + private static MarkerStorage.MapComponentStorage activeLabels; + private static File baseDir; private static String serverID; - private static StorageAccess.Internal storage; + private static InternalStorage storage; + private static PublicStorage addon; private static boolean isServerSource; private static String mdSource; + private static boolean running = false; public static void init() { BlazeNetwork.initEngine(); - MinecraftForge.EVENT_BUS.register(BlazeMapClientEngine.class); + MinecraftForge.EVENT_BUS.register(ClientEngine.class); } - public static StorageAccess.Internal getDimensionStorage(ResourceKey dimension) { + public static InternalStorage getDimensionStorage(ResourceKey dimension) { return storage.internal(dimension.location()); } @SubscribeEvent public static void onJoinServer(ClientPlayerNetworkEvent.LoggedInEvent event) { + if(running) throw new IllegalStateException("Cannot start twice"); + RegistryController.ensureRegistriesReady(); LocalPlayer player = event.getPlayer(); if(player == null) return; serverID = Helpers.getServerID(); - storage = new StorageAccess.Internal(Helpers.getClientSideStorageDir()); + storage = new InternalStorage(StorageType.SERVER, getStorageDir()); + addon = storage.addon(); isServerSource = detectRemote(event.getConnection()); - ServerJoinedEvent serverJoined = new ServerJoinedEvent(serverID, storage.addon(), isServerSource); - MinecraftForge.EVENT_BUS.post(serverJoined); - waypointStorageFactory = serverJoined.getWaypointStorageFactory(); - switchToPipeline(player.level.dimension()); mdSource = "unknown"; + running = true; + + ClientEngineEvent starting = new ClientEngineEvent.EngineStartingEvent(serverID, addon, isServerSource); + MinecraftForge.EVENT_BUS.post(starting); + + switchToPipeline(player.level.dimension()); + } + + private static File getStorageDir() { + Minecraft mc = Minecraft.getInstance(); + if(mc.hasSingleplayerServer()) { + return new File(mc.getSingleplayerServer().getWorldPath(LevelResource.ROOT).toFile(), "blazemap-client"); + } + else { + if(baseDir == null) baseDir = new File(mc.gameDirectory, "blazemap-servers"); + return new File(baseDir, mc.getCurrentServer().ip.replace(':', '+')); + } } private static boolean detectRemote(Connection connection) { @@ -83,17 +98,20 @@ private static boolean detectRemote(Connection connection) { @SubscribeEvent public static void onLeaveServer(ClientPlayerNetworkEvent.LoggedOutEvent event) { + if(!running) return; + + ClientEngineEvent stopping = new ClientEngineEvent.EngineStoppingEvent(serverID, addon, isServerSource); + MinecraftForge.EVENT_BUS.post(stopping); + PIPELINES.clear(); - WAYPOINTS.clear(); if(activePipeline != null) { activePipeline.shutdown(); activePipeline = null; } activeLabels = null; - activeWaypoints = null; serverID = null; storage = null; - waypointStorageFactory = null; + running = false; } @SubscribeEvent @@ -109,12 +127,7 @@ private static void switchToPipeline(ResourceKey dimension) { activePipeline = getPipeline(dimension); activeLabels = new LabelStorage(dimension); - IStorageAccess fileStorage = activePipeline.addonStorage; - activeWaypoints = WAYPOINTS.computeIfAbsent(dimension, d -> waypointStorageFactory.create( - () -> fileStorage.read(WAYPOINT_STORAGE), - () -> fileStorage.write(WAYPOINT_STORAGE), - () -> fileStorage.exists(WAYPOINT_STORAGE) - )); + StorageAccess fileStorage = activePipeline.addonStorage; TILE_CHANGE_LISTENERS.clear(); DimensionChangedEvent event = new DimensionChangedEvent( @@ -124,7 +137,6 @@ private static void switchToPipeline(ResourceKey dimension) { TILE_CHANGE_LISTENERS::add, activePipeline::consumeTile, activeLabels, - activeWaypoints, fileStorage ); MinecraftForge.EVENT_BUS.post(event); diff --git a/src/main/java/com/eerussianguy/blazemap/engine/client/ClientPipeline.java b/src/main/java/com/eerussianguy/blazemap/engine/client/ClientPipeline.java index bb7c3864..03855352 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/client/ClientPipeline.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/client/ClientPipeline.java @@ -21,11 +21,16 @@ import com.eerussianguy.blazemap.api.maps.*; import com.eerussianguy.blazemap.api.pipeline.*; import com.eerussianguy.blazemap.api.util.RegionPos; -import com.eerussianguy.blazemap.engine.*; -import com.eerussianguy.blazemap.engine.async.*; +import com.eerussianguy.blazemap.config.BlazeMapConfig; +import com.eerussianguy.blazemap.config.ServerConfig; +import com.eerussianguy.blazemap.engine.Pipeline; +import com.eerussianguy.blazemap.engine.PipelineProfiler; +import com.eerussianguy.blazemap.engine.UnsafeGenerics; import com.eerussianguy.blazemap.engine.cache.ChunkMDCache; import com.eerussianguy.blazemap.engine.cache.ChunkMDCacheView; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.engine.storage.InternalStorage; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.async.*; import com.mojang.blaze3d.platform.NativeImage; import static com.eerussianguy.blazemap.profiling.Profilers.Client.*; @@ -51,7 +56,7 @@ class ClientPipeline extends Pipeline { private final PriorityLock lock = new PriorityLock(); private boolean active, cold; - public ClientPipeline(AsyncChainRoot async, DebouncingThread debouncer, ResourceKey dimension, StorageAccess.Internal storage, PipelineType type) { + public ClientPipeline(AsyncChainRoot async, DebouncingThread debouncer, ResourceKey dimension, InternalStorage storage, PipelineType type) { super( async, debouncer, CLIENT_PIPELINE_PROFILER, dimension, Helpers::levelOrThrow, storage, computeCollectorSet(dimension, type), @@ -69,7 +74,7 @@ public ClientPipeline(AsyncChainRoot async, DebouncingThread debouncer, Resource // Set up views (immutable sets) for the available maps and layers. this.availableMapTypes = BlazeMapAPI.MAPTYPES.keys().stream().filter(m -> m.value().shouldRenderInDimension(dimension)).collect(Collectors.toUnmodifiableSet()); this.availableLayers = availableMapTypes.stream().map(k -> k.value().getLayers()).flatMap(Set::stream).filter(l -> l.value().shouldRenderInDimension(dimension)).collect(Collectors.toUnmodifiableSet()); - this.layers = availableLayers.stream().map(Key::value).filter(l -> !(l instanceof FakeLayer)).toArray(Layer[]::new); + this.layers = availableLayers.stream().map(Key::value).filter(l -> l.type.isPipelined).toArray(Layer[]::new); this.numLayers = layers.length; // Set up debouncing mechanisms @@ -78,7 +83,7 @@ public ClientPipeline(AsyncChainRoot async, DebouncingThread debouncer, Resource TILE_TIME_PROFILER.begin(); region.save(); TILE_TIME_PROFILER.end(); - }), 2500, 30000); + }), 2500, 30000, BlazeMap.LOGGER); this.useMDCache(); } @@ -88,7 +93,7 @@ private static Set>> computeCollectorSet(ResourceKey< .map(k -> k.value().getInputIDs()).map(ids -> BlazeMapAPI.COLLECTORS.keys().stream().filter(k -> ids.contains(k.value().getOutputID())) .collect(Collectors.toUnmodifiableSet())).flatMap(Set::stream).filter(k -> k.value().shouldExecuteIn(dimension, type)); - if(!BlazeMapClientEngine.isClientSource()) { + if(!ClientEngine.isClientSource()) { collectors = collectors.filter(k -> k.value() instanceof ClientOnlyCollector); } @@ -99,8 +104,13 @@ public void setHot() { cold = false; } + private boolean canPlayerWriteMap() { + return BlazeMapConfig.SERVER.mapItemRequirement.canPlayerAccessMap(Helpers.getPlayer(), ServerConfig.MapAccess.WRITE); + } + @Override public void insertMasterData(ChunkPos pos, List data) { + if(!canPlayerWriteMap()) return; AsyncChainItem chain = async.begin(); if(cold) chain = chain.thenDelay((int) (500 + ((System.nanoTime() / 1000) % 1000))); chain @@ -140,7 +150,7 @@ private Void deleteChunkTile(ChunkPos chunkPos) { NativeImage layerChunkTile = new NativeImage(NativeImage.Format.RGBA, resolution.chunkWidth, resolution.chunkWidth, true); for(Layer layer : layers) { Key layerID = layer.getID(); - LayerRegionTile layerRegionTile = getLayerRegionTile(layerID, regionPos, resolution, false); + LayerRegionTile layerRegionTile = getLayerRegionTile(layerID, regionPos, resolution); layerRegionTile.updateTile(layerChunkTile, chunkPos); updates.add(new LayerRegion(layerID, regionPos)); } @@ -151,6 +161,12 @@ private Void deleteChunkTile(ChunkPos chunkPos) { return null; } + @Override + protected void begin(ChunkPos pos) { + if(!canPlayerWriteMap()) return; + super.begin(pos); + } + // Redraw tiles based on MD changes // Check what MDs changed, mark dependent layers and processors as dirty // Ask layers to redraw tiles, if applicable: @@ -186,7 +202,7 @@ protected void onPipelineOutput(ChunkPos chunkPos, Set> diff, Chun if(layer.renderTile(layerChunkTile, resolution, view, xOff, zOff)) { // update this chunk of the region - LayerRegionTile layerRegionTile = getLayerRegionTile(layerID, regionPos, resolution, false); + LayerRegionTile layerRegionTile = getLayerRegionTile(layerID, regionPos, resolution); layerRegionTile.updateTile(layerChunkTile, chunkPos); // asynchronously save this region later @@ -209,7 +225,7 @@ protected void onPipelineOutput(ChunkPos chunkPos, Set> diff, Chun } } - private LayerRegionTile getLayerRegionTile(Key layer, RegionPos region, TileResolution resolution, boolean priority) { + private LayerRegionTile getLayerRegionTile(Key layer, RegionPos region, TileResolution resolution) { try { return tiles .computeIfAbsent(resolution, $ -> new ConcurrentHashMap<>()) @@ -236,7 +252,7 @@ public LayerRegionTile load(RegionPos pos) { private void sendMapUpdates(Set updates) { if(active) { for(LayerRegion update : updates) { - BlazeMapClientEngine.notifyLayerRegionChange(update); + ClientEngine.notifyLayerRegionChange(update); } } } @@ -260,9 +276,15 @@ public ClientPipeline activate() { return this; } - public void consumeTile(Key layer, RegionPos region, TileResolution resolution, Consumer consumer) { - if(!availableLayers.contains(layer)) - throw new IllegalArgumentException("Layer " + layer + " not available for dimension " + dimension); - getLayerRegionTile(layer, region, resolution, true).consume(consumer); + public void consumeTile(Key key, RegionPos region, TileResolution resolution, Consumer consumer) { + if(!availableLayers.contains(key)) { + throw new IllegalArgumentException("Layer " + key + " not available for dimension " + dimension); + } + Layer layer = key.value(); + switch(layer.type) { + case PHYSICAL -> getLayerRegionTile(key, region, resolution).consume(consumer); + case SYNTHETIC -> consumer.accept(((SyntheticLayer)layer).getPixelSource(dimension, region, resolution)); + case INVISIBLE -> throw new UnsupportedOperationException("Impossible to consume pixel data from invisible layer: " + key); + } } } diff --git a/src/main/java/com/eerussianguy/blazemap/engine/client/LabelStorage.java b/src/main/java/com/eerussianguy/blazemap/engine/client/LabelStorage.java index dbb9301e..e0ba5ad5 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/client/LabelStorage.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/client/LabelStorage.java @@ -1,7 +1,6 @@ package com.eerussianguy.blazemap.engine.client; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -12,13 +11,12 @@ import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.event.MapLabelEvent; -import com.eerussianguy.blazemap.api.maps.Layer; -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; +import com.eerussianguy.blazemap.api.maps.NamedMapComponent; +import com.eerussianguy.blazemap.api.markers.MarkerStorage; import com.eerussianguy.blazemap.api.markers.MapLabel; -class LabelStorage implements IMarkerStorage.Layered { - private final HashMap, HashMap> layers = new HashMap<>(); - private final HashMap, Collection> views = new HashMap<>(); +class LabelStorage implements MarkerStorage.MapComponentStorage, MarkerStorage { + private final HashMap>, MarkerStorageImpl> layers = new HashMap<>(); private final HashSet labelIDs = new HashSet<>(); private final ResourceKey dimension; @@ -27,49 +25,33 @@ public LabelStorage(ResourceKey dimension) { } @Override - public Collection getAll() { - throw new UnsupportedOperationException(); + public MarkerStorage getGlobal() { + return this; } @Override - public Collection getInLayer(Key layerID) { - return views.computeIfAbsent(layerID, l -> Collections.unmodifiableCollection(inLayer(l).values())); + public MarkerStorage getStorage(Key> componentID) { + return layers.computeIfAbsent(componentID, key -> new MarkerStorageImpl(key, dimension, labelIDs)); } @Override - public void add(MapLabel marker) { - if(!dimension.equals(marker.getDimension())) return; - ResourceLocation id = marker.getID(); - if(labelIDs.contains(id)) throw new IllegalStateException("Marker already exists in storage"); - inLayer(marker.getLayerID()).put(id, marker); - labelIDs.add(id); - MinecraftForge.EVENT_BUS.post(new MapLabelEvent.Created(marker)); + public Collection getAll() { + throw new UnsupportedOperationException("Cannot get all markers from global storage"); } @Override - public void remove(MapLabel label) { - ResourceLocation id = label.getID(); - if(labelIDs.contains(id)) { - inLayer(label.getLayerID()).remove(id); - labelIDs.remove(id); - MinecraftForge.EVENT_BUS.post(new MapLabelEvent.Removed(label)); - } + public void add(MapLabel marker) { + getStorage(marker.getComponentId()).add(marker); } @Override public void remove(ResourceLocation id) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("Cannot remove by ID in global storage"); } @Override - public void remove(ResourceLocation id, Key layerID) { - if(labelIDs.contains(id)) { - HashMap labels = inLayer(layerID); - if(!labels.containsKey(id)) throw new IllegalArgumentException("Marker is not in specified layer"); - MapLabel label = labels.remove(id); - labelIDs.remove(id); - MinecraftForge.EVENT_BUS.post(new MapLabelEvent.Removed(label)); - } + public void remove(MapLabel marker) { + getStorage(marker.getComponentId()).remove(marker.getID()); } @Override @@ -77,7 +59,58 @@ public boolean has(ResourceLocation id) { return labelIDs.contains(id); } - private HashMap inLayer(Key layer) { - return layers.computeIfAbsent(layer, l -> new HashMap<>()); + + private static class MarkerStorageImpl implements MarkerStorage { + private final HashMap markers = new HashMap<>(); + private final HashSet labelIDs; + private final ResourceKey dimension; + private final Key> componentID; + + private MarkerStorageImpl(Key> componentID, ResourceKey dimension, HashSet labelIDs) { + this.labelIDs = labelIDs; + this.dimension = dimension; + this.componentID = componentID; + } + + @Override + public Collection getAll() { + return markers.values(); + } + + @Override + public void add(MapLabel marker) { + if(!dimension.equals(marker.getDimension())) return; + if(!marker.getComponentId().equals(componentID)) throw new IllegalArgumentException("ComponentID mismatch"); + ResourceLocation id = marker.getID(); + if(labelIDs.contains(id)) throw new IllegalStateException("Marker already exists in storage"); + labelIDs.add(id); + markers.put(id, marker); + MinecraftForge.EVENT_BUS.post(new MapLabelEvent.Created(marker)); + } + + @Override + public void remove(MapLabel label) { + ResourceLocation id = label.getID(); + if(labelIDs.contains(id)) { + markers.remove(id); + labelIDs.remove(id); + MinecraftForge.EVENT_BUS.post(new MapLabelEvent.Removed(label)); + } + } + + @Override + public void remove(ResourceLocation id) { + if(labelIDs.contains(id)) { + if(!markers.containsKey(id)) throw new IllegalArgumentException("Marker is not in specified component"); + MapLabel label = markers.remove(id); + labelIDs.remove(id); + MinecraftForge.EVENT_BUS.post(new MapLabelEvent.Removed(label)); + } + } + + @Override + public boolean has(ResourceLocation id) { + return markers.containsKey(id); + } } } \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/engine/client/LayerRegionTile.java b/src/main/java/com/eerussianguy/blazemap/engine/client/LayerRegionTile.java index 93f5fb0f..d3c2054a 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/client/LayerRegionTile.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/client/LayerRegionTile.java @@ -13,9 +13,10 @@ import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.PixelSource; import com.eerussianguy.blazemap.api.maps.TileResolution; import com.eerussianguy.blazemap.api.util.RegionPos; -import com.eerussianguy.blazemap.engine.StorageAccess; +import com.eerussianguy.blazemap.engine.storage.InternalStorage; import com.eerussianguy.blazemap.profiling.Profilers; import com.mojang.blaze3d.platform.NativeImage; @@ -29,6 +30,7 @@ public class LayerRegionTile { private final File file, buffer; private NativeImage image; + private final PixelSource imageWrapper; private final TileResolution resolution; private volatile boolean isEmpty = true; private volatile boolean isDirty = false; @@ -42,10 +44,27 @@ private static String getBufferName(RegionPos region) { return region + ".buffer"; } - public LayerRegionTile(StorageAccess.Internal storage, BlazeRegistry.Key layer, RegionPos region, TileResolution resolution) { - this.file = storage.getMipmap(layer.location, getImageName(region), resolution); - this.buffer = storage.getMipmap(layer.location, getBufferName(region), resolution); + public LayerRegionTile(InternalStorage storage, BlazeRegistry.Key layer, RegionPos region, TileResolution resolution) { + this.file = storage.getMipmapDir(layer.location, getImageName(region), resolution); + this.buffer = storage.getMipmapDir(layer.location, getBufferName(region), resolution); this.resolution = resolution; + + imageWrapper = new PixelSource() { + @Override + public int getPixel(int x, int y) { + return image.getPixelRGBA(x, y); + } + + @Override + public int getWidth() { + return image.getWidth(); + } + + @Override + public int getHeight() { + return image.getHeight(); + } + }; } public void tryLoad() { @@ -161,12 +180,12 @@ public boolean isDirty() { return isDirty; } - public void consume(Consumer consumer) { + public void consume(Consumer consumer) { if(isEmpty) return; imageLock.readLock().lock(); try { - consumer.accept(image); + consumer.accept(imageWrapper); } finally { imageLock.readLock().unlock(); diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/MapRenderer.java b/src/main/java/com/eerussianguy/blazemap/engine/render/MapRenderer.java similarity index 56% rename from src/main/java/com/eerussianguy/blazemap/feature/maps/MapRenderer.java rename to src/main/java/com/eerussianguy/blazemap/engine/render/MapRenderer.java index 59c00f52..8889fe55 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/MapRenderer.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/render/MapRenderer.java @@ -1,10 +1,8 @@ -package com.eerussianguy.blazemap.feature.maps; +package com.eerussianguy.blazemap.engine.render; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; -import java.util.Random; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -12,21 +10,14 @@ import java.util.stream.Collectors; import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.player.LocalPlayer; -import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.animal.AbstractGolem; -import net.minecraft.world.entity.animal.Animal; -import net.minecraft.world.entity.animal.WaterAnimal; -import net.minecraft.world.entity.monster.Monster; -import net.minecraft.world.entity.npc.AbstractVillager; -import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; @@ -35,47 +26,35 @@ import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.event.DimensionChangedEvent; import com.eerussianguy.blazemap.api.event.MapLabelEvent; -import com.eerussianguy.blazemap.api.event.WaypointEvent; import com.eerussianguy.blazemap.api.maps.*; import com.eerussianguy.blazemap.api.markers.*; import com.eerussianguy.blazemap.api.util.RegionPos; import com.eerussianguy.blazemap.config.BlazeMapConfig; import com.eerussianguy.blazemap.engine.BlazeMapAsync; -import com.eerussianguy.blazemap.engine.async.AsyncAwaiter; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.feature.BlazeMapFeaturesClient; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.async.AsyncAwaiter; import com.eerussianguy.blazemap.profiling.Profiler; -import com.eerussianguy.blazemap.util.RenderHelper; import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.math.Matrix4f; -import com.mojang.math.Vector3f; public class MapRenderer implements AutoCloseable { - private static final ResourceLocation PLAYER = Helpers.identifier("textures/player.png"); + private static final ResourceLocation PLAYER = BlazeMap.resource("textures/player.png"); private static final List RENDERERS = new ArrayList<>(4); private static DimensionTileStorage tileStorage; private static ResourceKey dimension; - private static IMarkerStorage waypointStorage; - private static IMarkerStorage.Layered labelStorage; + private static MarkerStorage.MapComponentStorage labelStorage; public static void onDimensionChange(DimensionChangedEvent evt) { evt.tileNotifications.addUpdateListener(MapRenderer::onTileChanged); tileStorage = evt.tileStorage; dimension = evt.dimension; - waypointStorage = evt.waypoints; labelStorage = evt.labels; } - public static void onWaypointAdded(WaypointEvent.Created event) { - RENDERERS.forEach(r -> r.add(event.waypoint)); - } - - public static void onWaypointRemoved(WaypointEvent.Removed event) { - RENDERERS.forEach(r -> r.remove(event.waypoint)); - } - public static void onMapLabelAdded(MapLabelEvent.Created event) { RENDERERS.forEach(r -> r.add(event.label)); } @@ -85,23 +64,23 @@ public static void onMapLabelRemoved(MapLabelEvent.Removed event) { } private static void onTileChanged(LayerRegion tile) { - RENDERERS.forEach(r -> r.changed(tile.layer, tile.region)); + RENDERERS.forEach(r -> r.onLayerChanged(tile.layer, tile.region)); } // ================================================================================================================= - final DebugInfo debug = new DebugInfo(); + public final DebugInfo debug = new DebugInfo(); private Profiler.TimeProfiler renderTimer = new Profiler.TimeProfiler.Dummy(); private Profiler.TimeProfiler uploadTimer = new Profiler.TimeProfiler.Dummy(); private MapType mapType; - private List> disabled, visible; + private List> layers_on = new ArrayList<>(); + private List> overlays_on = new ArrayList<>(); + private final List> layers_off = new ArrayList<>(); + private final List> overlays_off = new ArrayList<>(); private final HashMap, List>> disabledLayers = new HashMap<>(); - private final List waypoints = new ArrayList<>(16); - private final List waypoints_on = new ArrayList<>(16); - private final List waypoints_off = new ArrayList<>(16); private final List labels = new ArrayList<>(16); private final List labels_on = new ArrayList<>(16); private final List labels_off = new ArrayList<>(16); @@ -113,6 +92,7 @@ private static void onTileChanged(LayerRegion tile) { private DynamicTexture mapTexture; private RenderType renderType; private boolean needsUpdate = true; + private final Marker playerMarker; private int width, height; private int mapWidth, mapHeight; @@ -121,10 +101,8 @@ private static void onTileChanged(LayerRegion tile) { private final double minZoom, maxZoom; private double zoom = 1; private TileResolution resolution; - private final boolean renderNames; - private final double playerMarkerZHeight = 1; - public MapRenderer(int width, int height, ResourceLocation textureResource, double minZoom, double maxZoom, boolean renderNames) { + public MapRenderer(int width, int height, ResourceLocation textureResource, double minZoom, double maxZoom) { this.center = new BlockPos.MutableBlockPos(); this.begin = new BlockPos.MutableBlockPos(); this.end = new BlockPos.MutableBlockPos(); @@ -133,6 +111,7 @@ public MapRenderer(int width, int height, ResourceLocation textureResource, doub this.maxZoom = maxZoom; resolution = TileResolution.FULL; + updateVisibleOverlays(); selectMapType(); centerOnPlayer(); @@ -140,17 +119,18 @@ public MapRenderer(int width, int height, ResourceLocation textureResource, doub this.resize(width, height); } - this.renderNames = renderNames; + LocalPlayer player = Helpers.getPlayer(); + playerMarker = new Marker<>(BlazeMap.resource("local_player"), player.level.dimension(), player.blockPosition(), PLAYER).setSize(32).setRotation(player.getRotationVector().y); RENDERERS.add(this); debug.zoom = zoom; } private void selectMapType() { - if(dimension != null && (mapType == null || !mapType.shouldRenderInDimension(dimension))) { + if(dimension != null && (mapType == null || !mapType.shouldRenderInDimension(dimension) || !BlazeMapConfig.SERVER.mapPermissions.isAllowed(mapType.getID()))) { for(BlazeRegistry.Key next : BlazeMapAPI.MAPTYPES.keys()) { MapType type = next.value(); - if(type.shouldRenderInDimension(dimension)) { + if(type.shouldRenderInDimension(dimension) && BlazeMapConfig.SERVER.mapPermissions.isAllowed(next)) { setMapType(type); break; } @@ -202,7 +182,6 @@ private void makeOffsets() { } } - updateWaypoints(); updateLabels(); // debug info @@ -212,42 +191,17 @@ private void makeOffsets() { debug.ez = end.getZ(); } - public void updateWaypoints() { - waypoints.clear(); - waypoints.addAll(waypointStorage.getAll().stream().filter(w -> inRange(w.getPosition())).collect(Collectors.toList())); - debug.waypoints = waypoints.size(); - waypoints.forEach(this::matchWaypoint); - pingSearchHost(); - } - - private void add(Waypoint waypoint) { - if(inRange(waypoint.getPosition())) { - waypoints.add(waypoint); - debug.waypoints++; - matchWaypoint(waypoint); - pingSearchHost(); - } - } - - private void remove(Waypoint waypoint) { - if(waypoints.remove(waypoint)) { - debug.waypoints--; - waypoints_on.remove(waypoint); - waypoints_off.remove(waypoint); - pingSearchHost(); - } - } - public void updateLabels() { labels.clear(); - visible.forEach(layer -> labels.addAll(labelStorage.getInLayer(layer).stream().filter(l -> inRange(l.getPosition())).collect(Collectors.toList()))); + layers_on.forEach(layer -> labels.addAll(labelStorage.getStorage(layer).getAll().stream().filter(l -> inRange(l.getPosition())).collect(Collectors.toList()))); + overlays_on.forEach(overlay -> labels.addAll(labelStorage.getStorage(overlay).getAll().stream().filter(o -> inRange(o.getPosition())).collect(Collectors.toList()))); debug.labels = labels.size(); labels.forEach(this::matchLabel); pingSearchHost(); } private void add(MapLabel label) { - if(inRange(label.getPosition()) && visible.contains(label.getLayerID())) { + if(inRange(label.getPosition()) && layers_on.contains(label.getComponentId())) { labels.add(label); debug.labels++; matchLabel(label); @@ -264,8 +218,8 @@ private void remove(MapLabel label) { } } - private void changed(BlazeRegistry.Key layer, RegionPos region) { - if(!visible.contains(layer)) return; + private void onLayerChanged(BlazeRegistry.Key layer, RegionPos region) { + if(!layers_on.contains(layer)) return; RegionPos r0 = offsets[0][0]; if(r0.x > region.x || r0.z > region.z) return; RegionPos[] arr = offsets[offsets.length - 1]; @@ -275,9 +229,25 @@ private void changed(BlazeRegistry.Key layer, RegionPos region) { } private void updateVisibleLayers() { - visible = mapType.getLayers().stream().filter(l -> !disabled.contains(l) && l.value().shouldRenderInDimension(dimension)).collect(Collectors.toList()); + layers_on = mapType.getLayers().stream().filter(this::isLayerAllowed).filter(this::isLayerVisible).toList(); updateLabels(); - debug.layers = visible.size(); + debug.layers = layers_on.size(); + } + + private boolean isLayerAllowed(BlazeRegistry.Key key) { + return key.value().shouldRenderInDimension(dimension) + && BlazeMapConfig.SERVER.layerPermissions.isAllowed(key); + } + + private void updateVisibleOverlays() { + overlays_on = BlazeMapFeaturesClient.OVERLAYS.stream().filter(this::isOverlayAllowed).filter(this::isOverlayVisible).toList(); + updateLabels(); + debug.overlays = overlays_on.size(); + } + + private boolean isOverlayAllowed(BlazeRegistry.Key key) { + return key.value().shouldRenderInDimension(dimension) + && BlazeMapConfig.SERVER.overlayPermissions.isAllowed(key); } private boolean inRange(BlockPos pos) { @@ -300,7 +270,16 @@ public void render(PoseStack stack, MultiBufferSource buffers) { RenderHelper.drawQuad(buffers.getBuffer(renderType), matrix, width, height); stack.pushPose(); - renderEntities(stack, buffers); + ClientLevel level = Minecraft.getInstance().level; + for(var key : overlays_on) { + stack.translate(0, 0, 0.1f); + stack.pushPose(); + key.value().getMarkers(level, resolution).forEach(marker -> { + stack.translate(0, 0, 0.0001f); + renderObject(buffers, stack, marker, SearchTargeting.NONE); + }); + stack.popPose(); + } stack.popPose(); stack.pushPose(); @@ -311,78 +290,22 @@ public void render(PoseStack stack, MultiBufferSource buffers) { for(MapLabel l : labels_on) { renderObject(buffers, stack, l, SearchTargeting.HIT); } - if (BlazeMapConfig.CLIENT.clientFeatures.displayWaypointsOnMap.get()) { - for(Waypoint w : waypoints_off) { - renderMarker(buffers, stack, w.getPosition(), w.getIcon(), w.getColor(), 32, 32, w.getRotation(), false, renderNames ? w.getName() : null, SearchTargeting.MISS); - } - for(Waypoint w : waypoints_on) { - renderMarker(buffers, stack, w.getPosition(), w.getIcon(), w.getColor(), 32, 32, w.getRotation(), false, renderNames ? w.getName() : null, SearchTargeting.HIT); - } - } } else { for(MapLabel l : labels) { renderObject(buffers, stack, l, SearchTargeting.NONE); } - if (BlazeMapConfig.CLIENT.clientFeatures.displayWaypointsOnMap.get()) { - for(Waypoint w : waypoints) { - renderMarker(buffers, stack, w.getPosition(), w.getIcon(), w.getColor(), 32, 32, w.getRotation(), false, renderNames ? w.getName() : null, SearchTargeting.NONE); - } - } } LocalPlayer player = Helpers.getPlayer(); - renderMarker(buffers, stack, player.blockPosition(), PLAYER, Colors.NO_TINT, 32, 32, playerMarkerZHeight, player.getRotationVector().y, false, null, SearchTargeting.NONE); + stack.translate(0, 0, 1); + playerMarker.setPosition(player.blockPosition()).setRotation(player.getRotationVector().y); + renderObject(buffers, stack, playerMarker, SearchTargeting.NONE); stack.popPose(); stack.popPose(); } - private void renderEntities(PoseStack stack, MultiBufferSource buffers) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - Random zHeightGenerator = new Random(); - mc.level.entitiesForRendering().forEach(entity -> { - if(!(entity instanceof LivingEntity)) return; - BlockPos pos = entity.blockPosition(); - - // Get a *consistent* random value for each entity to avoid appearance of z-fighting - zHeightGenerator.setSeed(entity.getUUID().getMostSignificantBits()); - double zHeight = zHeightGenerator.nextDouble(); - - if(inRange(pos)) { - int color; - boolean isPlayer = false; - if(entity instanceof Player && BlazeMapConfig.CLIENT.clientFeatures.displayOtherPlayers.get()) { - if(entity == player) return; - color = 0xFF88FF66; - isPlayer = true; - } - else if((entity instanceof AbstractVillager || entity instanceof AbstractGolem) && BlazeMapConfig.CLIENT.clientFeatures.displayFriendlyMobs.get()) { - color = 0xFFFFFF3F; - } - else if(entity instanceof Animal && BlazeMapConfig.CLIENT.clientFeatures.displayFriendlyMobs.get()) { - color = 0xFFA0A0A0; - } - else if(entity instanceof WaterAnimal && BlazeMapConfig.CLIENT.clientFeatures.displayFriendlyMobs.get()) { - color = 0xFF4488FF; - } - else if(entity instanceof Monster && BlazeMapConfig.CLIENT.clientFeatures.displayHostileMobs.get()) { - color = 0xFFFF2222; - } - else { - return; - } - - if(!isPlayer && Math.abs(player.position().y - entity.position().y) > 10) { - return; - } - - renderMarker(buffers, stack, pos, PLAYER, color, 32, 32, zHeight, entity.getRotationVector().y, false, isPlayer ? entity.getName().getString() : null, SearchTargeting.NONE); - } - }); - } - private void updateTexture() { NativeImage texture = mapTexture.getPixels(); if(texture == null) return; @@ -437,95 +360,76 @@ private void generateMapTileAsync(NativeImage texture, TileResolution resolution } private void generateMapTile(NativeImage texture, TileResolution resolution, int textureW, int textureH, int cornerXOffset, int cornerZOffset, int regionIndexX, int regionIndexZ) { - for(BlazeRegistry.Key layer : visible) { - if(layer.value() instanceof FakeLayer) return; - - // Precomputing values so they don't waste CPU cycles recalculating for each pixel - final RegionPos region = offsets[regionIndexX][regionIndexZ]; - final int cxo = cornerXOffset / resolution.pixelWidth; - final int czo = cornerZOffset / resolution.pixelWidth; - - final int regWidthbyIndexX = (regionIndexX * resolution.regionWidth); - final int regWidthbyIndexZ = (regionIndexZ * resolution.regionWidth); - - tileStorage.consumeTile(layer, region, resolution, source -> { - final int sourceWidth = source.getWidth(); - final int sourceHeight = source.getHeight(); - - int startX = (region.x * 512) < begin.getX() ? cxo : 0; - int startY = (region.z * 512) < begin.getZ() ? czo : 0; - - if (regWidthbyIndexX + startX - cxo < 0) { - // Set x to be the value it should be when textureX == 0 - startX = czo - regWidthbyIndexX; - } - if (regWidthbyIndexZ + startY - czo < 0) { - // Set y to be the value it should be when textureY == 0 - startY = czo - regWidthbyIndexZ; - } - - for(int x = startX; x < sourceWidth; x++) { - int textureX = regWidthbyIndexX + x - cxo; + // Precomputing values so they don't waste CPU cycles recalculating for each pixel + final RegionPos region = offsets[regionIndexX][regionIndexZ]; + final int cornerXOffsetScaled = cornerXOffset / resolution.pixelWidth; + final int cornerZOffsetScaled = cornerZOffset / resolution.pixelWidth; + final int regionFirstPixelX = (regionIndexX * resolution.regionWidth); + final int regionFirstPixelY = (regionIndexZ * resolution.regionWidth); + + int startX = (region.x * 512) < begin.getX() ? cornerXOffsetScaled : 0; + int startY = (region.z * 512) < begin.getZ() ? cornerZOffsetScaled : 0; + + if (regionFirstPixelX + startX - cornerXOffsetScaled < 0) { + // Set x to be the value it should be when textureX == 0 + startX = cornerXOffsetScaled - regionFirstPixelX; + } + if (regionFirstPixelY + startY - cornerZOffsetScaled < 0) { + // Set y to be the value it should be when textureY == 0 + startY = cornerZOffsetScaled - regionFirstPixelY; + } - if(textureX >= textureW) break; + final int _startX = startX, _startY = startY; + final int textureFirstPixelX = regionFirstPixelX - cornerXOffsetScaled; + final int textureFirstPixelY = regionFirstPixelY - cornerZOffsetScaled; - for(int y = startY; y < sourceHeight; y++) { - int textureY = regWidthbyIndexZ + y - czo; + // Paint map layers on the canvas + for(BlazeRegistry.Key layer : layers_on) { + if(!layer.value().type.isVisible) return; + tileStorage.consumeTile(layer, region, resolution, source -> transferPixels(texture, source, _startX, _startY, textureFirstPixelX, textureFirstPixelY, textureW, textureH)); + } - if(textureY >= textureH) break; + // Paint global overlays on top of the layers + for(BlazeRegistry.Key overlayKey : overlays_on) { + Overlay overlay = overlayKey.value(); + if(!overlay.type.isVisible) continue; - int color = Colors.layerBlend(texture.getPixelRGBA(textureX, textureY), source.getPixelRGBA(x, y)); - texture.setPixelRGBA(textureX, textureY, color); - } - } - }); + PixelSource source = overlay.getPixelSource(dimension, region, resolution); + transferPixels(texture, source, _startX, _startY, textureFirstPixelX, textureFirstPixelY, textureW, textureH); } } - private void renderMarker(MultiBufferSource buffers, PoseStack stack, BlockPos position, ResourceLocation marker, int color, double width, double height, float rotation, boolean zoom, String name, SearchTargeting search) { - renderMarker(buffers, stack, position, marker, color, width, height, 0, rotation, zoom, name, search); - } + private void transferPixels(NativeImage texture, PixelSource source, int startX, int startY, int textureFirstPixelX, int textureFirstPixelY, int textureW, int textureH) { + final int sourceWidth = source.getWidth(); + final int sourceHeight = source.getHeight(); + for(int x = startX; x < sourceWidth; x++) { + int textureX = textureFirstPixelX + x; - private void renderMarker(MultiBufferSource buffers, PoseStack stack, BlockPos position, ResourceLocation marker, int color, double width, double height, double zHeight, float rotation, boolean zoom, String name, SearchTargeting search) { - stack.pushPose(); + if(textureX >= textureW) break; - stack.scale((float) this.zoom, (float) this.zoom, 1); - int dx = position.getX() - begin.getX(); - int dy = position.getZ() - begin.getZ(); - stack.translate(dx, dy, 0); + for(int y = startY; y < sourceHeight; y++) { + int textureY = textureFirstPixelY + y; - if(!zoom) { - stack.scale(1F / (float) this.zoom, 1F / (float) this.zoom, 1); - } + if(textureY >= textureH) break; - if(name != null) { - float scale = 2; - Minecraft mc = Minecraft.getInstance(); - - stack.pushPose(); - stack.translate(-mc.font.width(name), (10 + (height / scale)), zHeight); - stack.scale(scale, scale, 0); - mc.font.drawInBatch(name, 0, 0, search.color(color), true, stack.last().pose(), buffers, false, 0, LightTexture.FULL_BRIGHT); - stack.popPose(); + int color = Colors.layerBlend(texture.getPixelRGBA(textureX, textureY), source.getPixel(x, y)); + texture.setPixelRGBA(textureX, textureY, color); + } } - stack.mulPose(Vector3f.ZP.rotationDegrees(rotation)); - stack.translate(-width / 2, -height / 2, zHeight); - VertexConsumer vertices = buffers.getBuffer(RenderType.text(marker)); - RenderHelper.drawQuad(vertices, stack.last().pose(), (float) width, (float) height, search.color(color)); - - stack.popPose(); } - private void renderObject(MultiBufferSource buffers, PoseStack stack, MapLabel label, SearchTargeting search) { + private void renderObject(MultiBufferSource buffers, PoseStack stack, Marker marker, SearchTargeting search) { + if(!inRange(marker.getPosition())) return; + stack.pushPose(); stack.scale((float) this.zoom, (float) this.zoom, 1); - BlockPos position = label.getPosition(); + BlockPos position = marker.getPosition(); int dx = position.getX() - begin.getX(); int dy = position.getZ() - begin.getZ(); stack.translate(dx, dy, 0); - ((ObjectRenderer) label.getRenderer().value()).render(label, stack, buffers, this.zoom, search); + ((ObjectRenderer>) marker.getRenderer().value()).render(marker, stack, buffers, this.zoom, search); stack.popPose(); } @@ -537,8 +441,6 @@ private void renderObject(MultiBufferSource buffers, PoseStack stack, MapLabel l public void setSearch(String search) { labels_off.clear(); labels_on.clear(); - waypoints_off.clear(); - waypoints_on.clear(); if(search == null || search.equals("")) { hasActiveSearch = false; matcher = null; @@ -553,7 +455,6 @@ public void setSearch(String search) { } hasActiveSearch = true; labels.forEach(this::matchLabel); - waypoints.forEach(this::matchWaypoint); } private void matchLabel(MapLabel label) { @@ -567,22 +468,13 @@ private void matchLabel(MapLabel label) { labels_off.add(label); } - private void matchWaypoint(Waypoint waypoint) { - if(!hasActiveSearch) return; - if(matcher.test(waypoint.getName())) { - waypoints_on.add(waypoint); - } else { - waypoints_off.add(waypoint); - } - } - public void setSearchHost(Consumer searchHost) { this.searchHost = searchHost; } public void pingSearchHost() { if(searchHost == null) return; - searchHost.accept(waypoints.size() + labels.size() > 0); + searchHost.accept(labels.size() > 0); } @@ -597,9 +489,11 @@ public boolean setMapType(MapType mapType) { } else { if(!mapType.shouldRenderInDimension(dimension)) return false; + if(!BlazeMapConfig.SERVER.mapPermissions.isAllowed(mapType.getID())) return false; this.mapType = mapType; } - this.disabled = disabledLayers.computeIfAbsent(this.mapType.getID(), $ -> new LinkedList<>()); + this.layers_off.clear(); + this.layers_off.addAll(disabledLayers.computeIfAbsent(this.mapType.getID(), $ -> new ArrayList<>())); updateVisibleLayers(); this.needsUpdate = true; return true; @@ -609,17 +503,28 @@ public MapType getMapType() { return mapType; } - List> getDisabledLayers() { - return this.disabled; + public List> getDisabledLayers() { + return this.layers_off; } - void setDisabledLayers(List> layers) { - this.disabled.clear(); - this.disabled.addAll(layers); + public void setDisabledLayers(List> layers) { + this.layers_off.clear(); + this.layers_off.addAll(layers); updateVisibleLayers(); this.needsUpdate = true; } + public List> getDisabledOverlays() { + return this.overlays_off; + } + + public void setDisabledOverlays(List> overlays) { + this.overlays_off.clear(); + this.overlays_off.addAll(overlays); + updateVisibleOverlays(); + this.needsUpdate = true; + } + public MapRenderer setProfilers(Profiler.TimeProfiler render, Profiler.TimeProfiler upload) { this.renderTimer = render; this.uploadTimer = upload; @@ -651,19 +556,36 @@ public int getBeginZ() { public boolean toggleLayer(BlazeRegistry.Key layer) { if(!mapType.getLayers().contains(layer)) return false; - if(disabled.contains(layer)) disabled.remove(layer); - else disabled.add(layer); + if(layers_off.contains(layer)) layers_off.remove(layer); + else layers_off.add(layer); updateVisibleLayers(); needsUpdate = true; return true; } - List> getVisibleLayers() { - return visible; + public List> getVisibleLayers() { + return layers_on; } public boolean isLayerVisible(BlazeRegistry.Key layer) { - return !disabled.contains(layer); + return !layers_off.contains(layer); + } + + public boolean toggleOverlay(BlazeRegistry.Key overlay) { + if(!overlay.value().shouldRenderInDimension(dimension)) return false; + if(overlays_off.contains(overlay)) overlays_off.remove(overlay); + else overlays_off.add(overlay); + updateVisibleOverlays(); + needsUpdate = true; + return true; + } + + public List> getVisibleOverlays() { + return overlays_on; + } + + public boolean isOverlayVisible(BlazeRegistry.Key overlay) { + return !overlays_off.contains(overlay); } public void setCenter(int x, int z) { @@ -704,12 +626,12 @@ public void close() { RENDERERS.remove(this); } - static class DebugInfo { - int rw, rh, mw, mh; - int bx, bz, ex, ez; - double zoom; - int ox, oz; - int layers, labels, waypoints; - String stitching; + public static class DebugInfo { + public int rw, rh, mw, mh; + public int bx, bz, ex, ez; + public double zoom; + public int ox, oz; + public int layers, overlays, labels; + public String stitching; } } diff --git a/src/main/java/com/eerussianguy/blazemap/engine/server/BlazeMapServerEngine.java b/src/main/java/com/eerussianguy/blazemap/engine/server/ServerEngine.java similarity index 84% rename from src/main/java/com/eerussianguy/blazemap/engine/server/BlazeMapServerEngine.java rename to src/main/java/com/eerussianguy/blazemap/engine/server/ServerEngine.java index c18963ab..75a47020 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/server/BlazeMapServerEngine.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/server/ServerEngine.java @@ -14,23 +14,25 @@ import net.minecraftforge.eventbus.api.SubscribeEvent; import com.eerussianguy.blazemap.api.BlazeMapAPI; +import com.eerussianguy.blazemap.api.event.ServerEngineEvent; import com.eerussianguy.blazemap.engine.BlazeMapAsync; import com.eerussianguy.blazemap.engine.Pipeline; import com.eerussianguy.blazemap.engine.RegistryController; -import com.eerussianguy.blazemap.engine.StorageAccess; +import com.eerussianguy.blazemap.engine.storage.InternalStorage; +import com.eerussianguy.blazemap.engine.storage.StorageType; import com.eerussianguy.blazemap.network.BlazeNetwork; import com.eerussianguy.blazemap.profiling.Profiler; -public class BlazeMapServerEngine { +public class ServerEngine { private static final Map, ServerPipeline> PIPELINES = new HashMap<>(); private static MinecraftServer server; private static boolean isRunning; - private static StorageAccess.Internal storage; + private static InternalStorage storage; private static int numCollectors = 0, numProcessors = 0, numTransformers = 0; public static void init() { BlazeNetwork.initEngine(); - MinecraftForge.EVENT_BUS.register(BlazeMapServerEngine.class); + MinecraftForge.EVENT_BUS.register(ServerEngine.class); } public static void submit(Runnable task) { @@ -43,12 +45,16 @@ public static void onServerStart(ServerStartingEvent event) { RegistryController.ensureRegistriesReady(); isRunning = true; server = event.getServer(); - storage = new StorageAccess.Internal(server.getWorldPath(LevelResource.ROOT).toFile(), "blazemap-server"); + storage = new InternalStorage(StorageType.SERVER, server.getWorldPath(LevelResource.ROOT).toFile(), "blazemap-server"); Profiler.setServerInstance(server); + + MinecraftForge.EVENT_BUS.post(new ServerEngineEvent.EngineStartingEvent(storage)); } @SubscribeEvent public static void onServerStop(ServerStoppedEvent event) { + MinecraftForge.EVENT_BUS.post(new ServerEngineEvent.EngineStoppingEvent(storage)); + isRunning = false; server = null; storage = null; diff --git a/src/main/java/com/eerussianguy/blazemap/engine/server/ServerPipeline.java b/src/main/java/com/eerussianguy/blazemap/engine/server/ServerPipeline.java index e1156b50..98e4728d 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/server/ServerPipeline.java +++ b/src/main/java/com/eerussianguy/blazemap/engine/server/ServerPipeline.java @@ -19,11 +19,14 @@ import com.eerussianguy.blazemap.api.pipeline.MasterDataDispatcher; import com.eerussianguy.blazemap.api.pipeline.MasterDatum; import com.eerussianguy.blazemap.api.pipeline.PipelineType; -import com.eerussianguy.blazemap.engine.*; -import com.eerussianguy.blazemap.engine.async.AsyncChainRoot; -import com.eerussianguy.blazemap.engine.async.DebouncingThread; +import com.eerussianguy.blazemap.engine.Pipeline; +import com.eerussianguy.blazemap.engine.PipelineProfiler; +import com.eerussianguy.blazemap.engine.storage.InternalStorage; +import com.eerussianguy.blazemap.engine.UnsafeGenerics; import com.eerussianguy.blazemap.engine.cache.ChunkMDCache; import com.eerussianguy.blazemap.engine.cache.ChunkMDCacheView; +import com.eerussianguy.blazemap.lib.async.AsyncChainRoot; +import com.eerussianguy.blazemap.lib.async.DebouncingThread; import com.eerussianguy.blazemap.network.PacketChunkMDUpdate; import static com.eerussianguy.blazemap.profiling.Profilers.Server.*; @@ -40,7 +43,7 @@ class ServerPipeline extends Pipeline { private final MasterDataDispatcher dispatcher; - public ServerPipeline(AsyncChainRoot async, DebouncingThread debouncer, ResourceKey dimension, Supplier level, StorageAccess.Internal storage) { + public ServerPipeline(AsyncChainRoot async, DebouncingThread debouncer, ResourceKey dimension, Supplier level, InternalStorage storage) { super( async, debouncer, SERVER_PIPELINE_PROFILER, dimension, level, storage, BlazeMapAPI.COLLECTORS.keys().stream().filter(k -> k.value().shouldExecuteIn(dimension, PipelineType.SERVER)).collect(Collectors.toUnmodifiableSet()), @@ -56,7 +59,7 @@ public ServerPipeline(AsyncChainRoot async, DebouncingThread debouncer, Resource @Override @SuppressWarnings("rawtypes") protected void onPipelineOutput(ChunkPos pos, Set> diff, ChunkMDCacheView view, ChunkMDCache cache) { - dispatcher.dispatch(dimension, pos, cache.data(), UnsafeGenerics.mdKeys(diff), BlazeMapServerEngine.getMDSource(), level.get().getChunk(pos.x, pos.z)); + dispatcher.dispatch(dimension, pos, cache.data(), UnsafeGenerics.mdKeys(diff), ServerEngine.getMDSource(), level.get().getChunk(pos.x, pos.z)); } private void dispatch(ResourceKey dimension, ChunkPos pos, List data, Set>> diff, String source, LevelChunk chunk) { diff --git a/src/main/java/com/eerussianguy/blazemap/engine/storage/InternalStorage.java b/src/main/java/com/eerussianguy/blazemap/engine/storage/InternalStorage.java new file mode 100644 index 00000000..b6841337 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/engine/storage/InternalStorage.java @@ -0,0 +1,88 @@ +package com.eerussianguy.blazemap.engine.storage; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.api.maps.TileResolution; + +public class InternalStorage extends PublicStorage { + private static final String PATTERN_MIP = PATTERN + " [%d]"; + private static final String PATTERN_OLD = "%s+%s"; + private static final Pattern PATTERN_OLD_REGEX = Pattern.compile("^([a-z0-9_]+)\\+([a-z0-9_]+)$"); + + public InternalStorage(StorageType type, File dir, String child) { + this(type, new File(dir, child)); + } + + public InternalStorage(StorageType type, File dir) { + super(type, dir); + dir.mkdirs(); + } + + public File getMipmapDir(ResourceLocation node, String file, TileResolution resolution) { + Objects.requireNonNull(node); + File d = new File(dir, String.format(PATTERN_MIP, node.getNamespace(), node.getPath(), resolution.pixelWidth)); + d.mkdirs(); + return new File(d, file); + } + + /** + * Port files from their v0.4 location to their v0.5 location + */ + public void tryPortDimension(ResourceLocation node) throws IOException { + Objects.requireNonNull(node); + File newFile = getDir(node).getParentFile(); + + if(!newFile.exists() || + (newFile.isDirectory() && newFile.list().length == 0)) { + File oldFile = new File(dir.getParent(), String.format(PATTERN_OLD, node.getNamespace(), node.getPath())); + oldFile.getParentFile().mkdirs(); + + if(oldFile.exists()) { + move(oldFile, newFile); + + File[] layers = newFile.listFiles(); + for(File layer : layers) { + portLayer(layer); + } + } + } + } + + public void portLayer(File oldFile) throws IOException { + String oldFilename = oldFile.getName(); + Matcher matcher = PATTERN_OLD_REGEX.matcher(oldFilename); + + if(matcher.matches()) { + File newFile = new File(dir, String.format(PATTERN_MIP, matcher.group(1), matcher.group(2), TileResolution.FULL.pixelWidth)); + newFile.mkdirs(); + + move(oldFile, newFile); + } + } + + public PublicStorage addon() { + return new PublicStorage(type, dir); + } + + public PublicStorage addon(ResourceLocation node) { + return new PublicStorage(childType(), getDir(node)); + } + + public InternalStorage internal(ResourceLocation node) { + return new InternalStorage(childType(), getDir(node)); + } + + protected StorageType childType() { + return switch(type) { + case GLOBAL -> StorageType.SERVER; + case SERVER -> StorageType.LEVEL; + default -> StorageType.GENERIC; + }; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/engine/storage/PublicStorage.java b/src/main/java/com/eerussianguy/blazemap/engine/storage/PublicStorage.java new file mode 100644 index 00000000..2fe8a5f8 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/engine/storage/PublicStorage.java @@ -0,0 +1,142 @@ +package com.eerussianguy.blazemap.engine.storage; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.api.util.StorageAccess; +import com.eerussianguy.blazemap.api.util.MinecraftStreams; + +public class PublicStorage implements StorageAccess.LevelStorage, StorageAccess.ServerStorage, StorageAccess.GlobalStorage { + protected static final String PATTERN = "[%s] %s"; + protected final StorageType type; + protected final File dir; + + public PublicStorage(StorageType type, File dir) { + this.type = type; + this.dir = dir; + } + + // GENERIC STORAGE ================================================================================================= + @Override + public boolean exists(ResourceLocation node) { + return getFile(node).exists(); + } + + @Override + public boolean exists(ResourceLocation node, String child) { + return getFile(node, child).exists(); + } + + @Override + public MinecraftStreams.Input read(ResourceLocation node) throws IOException { + return new MinecraftStreams.Input(new FileInputStream(getFile(node))); + } + + @Override + public MinecraftStreams.Input read(ResourceLocation node, String child) throws IOException { + return new MinecraftStreams.Input(new FileInputStream(getFile(node, child))); + } + + @Override + public MinecraftStreams.Output write(ResourceLocation node) throws IOException { + return new MinecraftStreams.Output(new FileOutputStream(getFile(node))); + } + + @Override + public MinecraftStreams.Output write(ResourceLocation node, String child) throws IOException { + return new MinecraftStreams.Output(new FileOutputStream(getFile(node, child))); + } + + @Override + public void move(ResourceLocation source, ResourceLocation destination) throws IOException { + move(getFile(source), getFile(destination)); + } + + @Override + public void move(ResourceLocation node, String source, String destination) throws IOException { + move(getFile(node, source), getFile(node, destination)); + } + + // LEVEL STORAGE =================================================================================================== + + // SERVER STORAGE ================================================================================================== + @Override + public LevelStorage getLevelStorage(ResourceLocation dimension) { + checkServer(); + return new PublicStorage(StorageType.LEVEL, getDir(dimension)); + } + + @Override + public void forEachLevel(BiConsumer consumer) { + checkServer(); + + var files = dir.listFiles((dir1, name) -> name.startsWith("[")); + if(files == null) return; + + for(var dir : files) { + var key = new ResourceLocation(dir.getName().replace("[", "").replace("] ", ":")); + consumer.accept(key, new PublicStorage(StorageType.LEVEL, dir)); + } + } + + // GLOBAL STORAGE ================================================================================================== + @Override + public void foreachSave(Consumer save) { + checkGlobal(); + + } + + @Override + public void foreachServer(Consumer server) { + checkGlobal(); + + } + + // INTERNALS ======================================================================================================= + protected void move(File source, File destination) throws IOException { + Files.move(source.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + protected File getFile(ResourceLocation node) { + Objects.requireNonNull(node); + File mod = new File(dir, node.getNamespace()); + File file = new File(mod, node.getPath()); + file.getParentFile().mkdirs(); + return file; + } + + protected File getFile(ResourceLocation node, String child) { + Objects.requireNonNull(node); + File dir = getFile(node); + dir.mkdirs(); + return new File(dir, child); + } + + protected File getDir(ResourceLocation node) { + Objects.requireNonNull(node); + File file = new File(dir, String.format(PATTERN, node.getNamespace(), node.getPath())); + file.getParentFile().mkdirs(); + return file; + } + + protected void checkLevel() { + if(type != StorageType.LEVEL) throw new UnsupportedOperationException("Can only be done on LevelStorage"); + } + + protected void checkServer() { + if(type != StorageType.SERVER) throw new UnsupportedOperationException("Can only be done on ServerStorage"); + } + + protected void checkGlobal() { + if(type != StorageType.GLOBAL) throw new UnsupportedOperationException("Can only be done on GlobalStorage"); + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/engine/storage/StorageType.java b/src/main/java/com/eerussianguy/blazemap/engine/storage/StorageType.java new file mode 100644 index 00000000..4e63bc99 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/engine/storage/StorageType.java @@ -0,0 +1,5 @@ +package com.eerussianguy.blazemap.engine.storage; + +public enum StorageType { + GENERIC, LEVEL, SERVER, GLOBAL; +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/feature/BlazeMapCommandsClient.java b/src/main/java/com/eerussianguy/blazemap/feature/BlazeMapCommandsClient.java index 82b2ced6..277809b3 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/BlazeMapCommandsClient.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/BlazeMapCommandsClient.java @@ -31,12 +31,12 @@ private static LiteralArgumentBuilder createDebug() { return Commands.literal("debug") .then(Commands.literal("on").executes($ -> { BlazeMapConfig.CLIENT.enableDebug.set(true); - Overlays.reload(); + IngameOverlays.reload(); return Command.SINGLE_SUCCESS; })) .then(Commands.literal("off").executes($ -> { BlazeMapConfig.CLIENT.enableDebug.set(false); - Overlays.reload(); + IngameOverlays.reload(); return Command.SINGLE_SUCCESS; })); } @@ -45,12 +45,12 @@ private static LiteralArgumentBuilder createMinimap() { return Commands.literal("minimap") .then(Commands.literal("on").executes($ -> { BlazeMapConfig.CLIENT.minimap.enabled.set(true); - Overlays.reload(); + IngameOverlays.reload(); return Command.SINGLE_SUCCESS; })) .then(Commands.literal("off").executes($ -> { BlazeMapConfig.CLIENT.minimap.enabled.set(false); - Overlays.reload(); + IngameOverlays.reload(); return Command.SINGLE_SUCCESS; })); } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/BlazeMapFeaturesClient.java b/src/main/java/com/eerussianguy/blazemap/feature/BlazeMapFeaturesClient.java index 066bd930..99f1b5db 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/BlazeMapFeaturesClient.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/BlazeMapFeaturesClient.java @@ -1,5 +1,9 @@ package com.eerussianguy.blazemap.feature; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + import org.lwjgl.glfw.GLFW; import net.minecraft.client.KeyMapping; import net.minecraft.client.gui.screens.Screen; @@ -7,34 +11,66 @@ import net.minecraftforge.client.event.InputEvent; import net.minecraftforge.client.settings.KeyConflictContext; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.eventbus.api.IEventBus; import com.eerussianguy.blazemap.BlazeMap; -import com.eerussianguy.blazemap.config.BlazeMapConfig; import com.eerussianguy.blazemap.api.BlazeMapAPI; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.event.BlazeRegistriesFrozenEvent; +import com.eerussianguy.blazemap.api.event.ComponentOrderingEvent.OverlayOrderingEvent; import com.eerussianguy.blazemap.api.event.MapMenuSetupEvent; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.config.BlazeMapConfig; +import com.eerussianguy.blazemap.config.ServerConfig; +import com.eerussianguy.blazemap.engine.render.MapRenderer; import com.eerussianguy.blazemap.feature.mapping.*; import com.eerussianguy.blazemap.feature.maps.*; -import com.eerussianguy.blazemap.feature.waypoints.WaypointEditorGui; -import com.eerussianguy.blazemap.feature.waypoints.WaypointManagerGui; -import com.eerussianguy.blazemap.feature.waypoints.WaypointRenderer; -import com.eerussianguy.blazemap.feature.waypoints.WaypointStore; +import com.eerussianguy.blazemap.feature.overlays.EntityOverlay; +import com.eerussianguy.blazemap.feature.overlays.GridOverlay; +import com.eerussianguy.blazemap.feature.waypoints.*; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointService; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointServiceClient; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointServiceServer; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.platform.InputConstants; public class BlazeMapFeaturesClient { + private static final LinkedHashSet> MUT_OVERLAYS = new LinkedHashSet<>(); + public static final Set> OVERLAYS = Collections.unmodifiableSet(MUT_OVERLAYS); + public static final KeyMapping KEY_MAPS = new KeyMapping("blazemap.key.maps", KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_B, BlazeMap.MOD_NAME); public static final KeyMapping KEY_ZOOM = new KeyMapping("blazemap.key.zoom", KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_LEFT_BRACKET, BlazeMap.MOD_NAME); public static final KeyMapping KEY_WAYPOINTS = new KeyMapping("blazemap.key.waypoints", KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_N, BlazeMap.MOD_NAME); + public static final KeyMapping KEY_TOGGLE_MINIMAP = new KeyMapping("blazemap.key.toggle_minimap", KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_BACKSLASH, BlazeMap.MOD_NAME); private static boolean mapping = false; private static boolean maps = false; private static boolean waypoints = false; + private static boolean overlays = false; public static boolean hasMapping() { return mapping; } + public static boolean hasMaps() { + return maps; + } + + public static boolean hasWaypoints() { + return waypoints && + (BlazeMapConfig.CLIENT.clientFeatures.displayWaypointsOnMap.get() || + BlazeMapConfig.CLIENT.clientFeatures.renderWaypointsInWorld.get()); + } + + public static boolean hasWaypointsOnMap() { + return waypoints && BlazeMapConfig.CLIENT.clientFeatures.displayWaypointsOnMap.get(); + } + + public static boolean hasOverlays() { + return overlays; + } + public static void initMapping() { BlazeMapAPI.LAYERS.register(new TerrainHeightLayer()); BlazeMapAPI.LAYERS.register(new TerrainSlopeLayer()); @@ -50,14 +86,21 @@ public static void initMapping() { mapping = true; } - public static boolean hasMaps() { - return maps; + public static void initOverlays() { + BlazeMapAPI.OVERLAYS.register(new GridOverlay()); + BlazeMapAPI.OVERLAYS.register(new WaypointOverlay()); + BlazeMapAPI.OVERLAYS.register(new EntityOverlay.Players()); + BlazeMapAPI.OVERLAYS.register(new EntityOverlay.NPCs()); + BlazeMapAPI.OVERLAYS.register(new EntityOverlay.Animals()); + BlazeMapAPI.OVERLAYS.register(new EntityOverlay.Enemies()); + overlays = true; } public static void initMaps() { ClientRegistry.registerKeyBinding(KEY_MAPS); ClientRegistry.registerKeyBinding(KEY_ZOOM); ClientRegistry.registerKeyBinding(KEY_WAYPOINTS); + ClientRegistry.registerKeyBinding(KEY_TOGGLE_MINIMAP); BlazeMapAPI.OBJECT_RENDERERS.register(new DefaultObjectRenderer()); @@ -65,37 +108,71 @@ public static void initMaps() { bus.addListener(MapRenderer::onDimensionChange); bus.addListener(MapRenderer::onMapLabelAdded); bus.addListener(MapRenderer::onMapLabelRemoved); + bus.addListener(BlazeMapFeaturesClient::mapOverlays); bus.addListener(BlazeMapFeaturesClient::mapKeybinds); bus.addListener(BlazeMapFeaturesClient::mapMenu); maps = true; } + private static void mapOverlays(BlazeRegistriesFrozenEvent evt) { + OverlayOrderingEvent event = new OverlayOrderingEvent(MUT_OVERLAYS); + event.add(BlazeMapReferences.Overlays.GRID); + if(hasWaypointsOnMap()) { + event.add(BlazeMapReferences.Overlays.WAYPOINTS); + } + event.add( + BlazeMapReferences.Overlays.PLAYERS, + BlazeMapReferences.Overlays.NPCS, + BlazeMapReferences.Overlays.ANIMALS, + BlazeMapReferences.Overlays.ENEMIES + ); + MinecraftForge.EVENT_BUS.post(event); + event.finish(); + overlays = MUT_OVERLAYS.size() > 0; + } + private static void mapKeybinds(InputEvent.KeyInputEvent evt) { if(KEY_MAPS.isDown()) { if(Screen.hasShiftDown()) { - MinimapOptionsGui.open(); + executeOrNotify(ServerConfig.MapAccess.READ_LIVE, MinimapOptionsGui::open); } else { - WorldMapGui.open(); + executeOrNotify(ServerConfig.MapAccess.READ_STATIC, WorldMapGui::open); } } if(KEY_WAYPOINTS.isDown() && hasWaypoints()) { if(Screen.hasShiftDown()) { - WaypointManagerGui.open(); + executeOrNotify(ServerConfig.MapAccess.READ_LIVE, () -> new WaypointManagerFragment().open()); } else { - WaypointEditorGui.open(); + executeOrNotify(ServerConfig.MapAccess.READ_LIVE, () -> new WaypointEditorFragment().open()); } } if(KEY_ZOOM.isDown()) { - if(Screen.hasShiftDown()) { - MinimapRenderer.INSTANCE.synchronizer.zoomOut(); - } - else { - MinimapRenderer.INSTANCE.synchronizer.zoomIn(); + if(BlazeMapConfig.SERVER.mapItemRequirement.canPlayerAccessMap(Helpers.getPlayer(), ServerConfig.MapAccess.READ_LIVE)) { + if(Screen.hasShiftDown()) { + MinimapRenderer.INSTANCE.synchronizer.zoomOut(); + } + else { + MinimapRenderer.INSTANCE.synchronizer.zoomIn(); + } } } + if (KEY_TOGGLE_MINIMAP.isDown()) { + boolean currentMinimapEnabledValue = BlazeMapConfig.CLIENT.minimap.enabled.get(); + BlazeMapConfig.CLIENT.minimap.enabled.set(!currentMinimapEnabledValue); + IngameOverlays.reload(); + } + } + + /** If the player has enough map access level, executes provided action. Otherwise notifies the player. */ + private static void executeOrNotify(ServerConfig.MapAccess access, Runnable action) { + if(BlazeMapConfig.SERVER.mapItemRequirement.canPlayerAccessMap(Helpers.getPlayer(), access)) { + action.run(); + } else { + Helpers.getPlayer().displayClientMessage(Helpers.translate("blazemap.gui.notification.denied"), true); + } } private static void mapMenu(MapMenuSetupEvent evt) { @@ -107,22 +184,13 @@ private static void mapMenu(MapMenuSetupEvent evt) { } } - public static boolean hasWaypoints() { - return waypoints && - (BlazeMapConfig.CLIENT.clientFeatures.displayWaypointsOnMap.get() || - BlazeMapConfig.CLIENT.clientFeatures.renderWaypointsInWorld.get()); - } - public static void initWaypoints() { IEventBus bus = MinecraftForge.EVENT_BUS; - bus.addListener(WaypointEditorGui::onDimensionChanged); - bus.addListener(WaypointManagerGui::onDimensionChanged); - bus.addListener(EventPriority.HIGHEST, WaypointStore::onServerJoined); - bus.addListener(MapRenderer::onWaypointAdded); - bus.addListener(MapRenderer::onWaypointRemoved); - bus.addListener(WorldMapMenu::trackWaypointStore); + bus.register(WaypointServiceClient.class); + bus.register(WaypointServiceServer.class); WaypointRenderer.init(); + WaypointService.init(); waypoints = true; } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/Overlays.java b/src/main/java/com/eerussianguy/blazemap/feature/IngameOverlays.java similarity index 79% rename from src/main/java/com/eerussianguy/blazemap/feature/Overlays.java rename to src/main/java/com/eerussianguy/blazemap/feature/IngameOverlays.java index 91ecc2d8..5081a251 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/Overlays.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/IngameOverlays.java @@ -6,16 +6,18 @@ import net.minecraftforge.client.gui.OverlayRegistry; import com.eerussianguy.blazemap.config.BlazeMapConfig; +import com.eerussianguy.blazemap.config.ServerConfig; import com.eerussianguy.blazemap.feature.maps.MinimapRenderer; +import com.eerussianguy.blazemap.lib.Helpers; import com.eerussianguy.blazemap.profiling.overlay.ProfilingRenderer; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.Tesselator; import static com.eerussianguy.blazemap.BlazeMap.MOD_NAME; -public class Overlays { - public static final IIngameOverlay MINIMAP = OverlayRegistry.registerOverlayTop(MOD_NAME + " Minimap", Overlays::renderMinimap); - public static final IIngameOverlay PROFILER = OverlayRegistry.registerOverlayTop(MOD_NAME + " Profiler", Overlays::renderProfiler); +public class IngameOverlays { + public static final IIngameOverlay MINIMAP = OverlayRegistry.registerOverlayTop(MOD_NAME + " Minimap", IngameOverlays::renderMinimap); + public static final IIngameOverlay PROFILER = OverlayRegistry.registerOverlayTop(MOD_NAME + " Profiler", IngameOverlays::renderProfiler); public static void reload() { OverlayRegistry.enableOverlay(MINIMAP, BlazeMapConfig.CLIENT.minimap.enabled.get()); @@ -23,6 +25,8 @@ public static void reload() { } public static void renderMinimap(ForgeIngameGui gui, PoseStack stack, float partialTicks, int width, int height) { + if(!BlazeMapConfig.SERVER.mapItemRequirement.canPlayerAccessMap(Helpers.getPlayer(), ServerConfig.MapAccess.READ_LIVE)) return; + stack.pushPose(); var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); MinimapRenderer.INSTANCE.draw(stack, buffers, gui, width, height); diff --git a/src/main/java/com/eerussianguy/blazemap/feature/ModIntegration.java b/src/main/java/com/eerussianguy/blazemap/feature/ModIntegration.java deleted file mode 100644 index a4a673fe..00000000 --- a/src/main/java/com/eerussianguy/blazemap/feature/ModIntegration.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.eerussianguy.blazemap.feature; - -import java.util.List; - -public class ModIntegration { - public static class ModIDs { - public static final String MINECRAFT = "minecraft"; - public static final String FORGE = "forge"; - - public static final String SODIUM = "sodium"; - public static final String EMBEDDIUM = "embeddium"; - public static final String RUBIDIUM = "rubidium"; - - public static final String OPTIFINE = "optifine"; - public static final String CHUNKPREGEN = "chunkpregen"; - } - - public static final List SODIUM_FAMILY = List.of(ModIDs.SODIUM, ModIDs.EMBEDDIUM, ModIDs.RUBIDIUM); -} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/atlas/AtlasExportProgress.java b/src/main/java/com/eerussianguy/blazemap/feature/atlas/AtlasExportProgress.java new file mode 100644 index 00000000..d4e78cb5 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/atlas/AtlasExportProgress.java @@ -0,0 +1,62 @@ +package com.eerussianguy.blazemap.feature.atlas; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; + +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.mojang.blaze3d.vertex.PoseStack; + +public class AtlasExportProgress extends BaseComponent { + private final float scale = (float) Minecraft.getInstance().getWindow().getGuiScale() / 2F; + private final Font font = Minecraft.getInstance().font; + + public AtlasExportProgress() { + setSize((int)(200 / scale), (int)(30 / scale)); + } + + @Override + public boolean isVisible() { + return AtlasExporter.getTask() != null; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + AtlasTask task = AtlasExporter.getTask(); + if(task == null) return; + + // draw background at regular scale to match size + RenderHelper.fillRect(stack.last().pose(), getWidth(), getHeight(), Colors.WIDGET_BACKGROUND); + + // scale contents down + stack.scale(1F / scale, 1F / scale, 1F); + + // Process flashing "animation" + int textColor = Colors.WHITE; + long flashUntil = ((long)task.getFlashUntil()) * 1000L; + long now = System.currentTimeMillis(); + if(task.isErrored() || (flashUntil >= now && now % 333 < 166)) { + textColor = 0xFFFF0000; + } + + // Render progress text + int total = task.getTilesTotal(); + int current = task.getTilesCurrent(); + font.draw(stack, String.format("Exporting 1:%d", task.resolution.pixelWidth), 5, 5, textColor); + String operation = switch(task.getStage()){ + case QUEUED -> "queued"; + case CALCULATING -> "calculating"; + case STITCHING -> String.format("stitching %d / %d tiles", current, total); + case SAVING -> "saving"; + case COMPLETE -> "complete"; + }; + font.draw(stack, operation, 195 - font.width(operation), 5, textColor); + + // Render progress bar + double progress = ((double)current) / ((double)total); + stack.translate(5, 17, 0); + RenderHelper.fillRect(stack.last().pose(), 190, 10, Colors.LABEL_COLOR); + RenderHelper.fillRect(stack.last().pose(), (int)(190*progress), 10, textColor); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/atlas/AtlasTask.java b/src/main/java/com/eerussianguy/blazemap/feature/atlas/AtlasTask.java index 24699e96..c227814b 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/atlas/AtlasTask.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/atlas/AtlasTask.java @@ -20,11 +20,11 @@ import com.eerussianguy.blazemap.api.maps.TileResolution; import com.eerussianguy.blazemap.api.util.RegionPos; import com.eerussianguy.blazemap.config.BlazeMapConfig; -import com.eerussianguy.blazemap.engine.StorageAccess; -import com.eerussianguy.blazemap.engine.client.BlazeMapClientEngine; +import com.eerussianguy.blazemap.engine.storage.InternalStorage; +import com.eerussianguy.blazemap.engine.client.ClientEngine; import com.eerussianguy.blazemap.engine.client.LayerRegionTile; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.platform.NativeImage; public class AtlasTask { @@ -95,7 +95,7 @@ void exportAsync() { try { // Calculation stage: figure out image size and corner regions, create canvas of appropriate size setStage(Stage.CALCULATING); - StorageAccess.Internal storage = BlazeMapClientEngine.getDimensionStorage(dimension); + InternalStorage storage = ClientEngine.getDimensionStorage(dimension); final NativeImage atlas = image = constructAtlas(storage, resolution); // Stitching stage: open tiles and transfer pixels to the correct place in the atlas @@ -104,7 +104,7 @@ void exportAsync() { for(var layerKey : map.value().getLayers()) { // Loop layers if(!layers.contains(layerKey)) continue; - File folder = storage.getMipmap(layerKey.location, ".", resolution); + File folder = storage.getMipmapDir(layerKey.location, ".", resolution); pages.forEach(page -> renderAtlasPage(page, folder, atlas, origin)); } @@ -193,14 +193,14 @@ private static NativeImage readTile(File file) { } } - private NativeImage constructAtlas(StorageAccess.Internal storage, TileResolution resolution) { + private NativeImage constructAtlas(InternalStorage storage, TileResolution resolution) { Set regions = new HashSet<>(); var layers = map.value().getLayers(); // Determine which regions of the map will need to be rendered for(var layer : layers) { if(!layers.contains(layer)) continue; - File folder = storage.getMipmap(layer.location, ".", resolution); + File folder = storage.getMipmapDir(layer.location, ".", resolution); File[] images = folder.listFiles(); if(images == null) continue; @@ -305,13 +305,6 @@ private void calculateAtlasSize() { } private static File getExportFile() { - Calendar calendar = Calendar.getInstance(); - int year = calendar.get(Calendar.YEAR); - int month = calendar.get(Calendar.MONTH) + 1; // fuck you too Java - int day = calendar.get(Calendar.DAY_OF_MONTH); - int hour = calendar.get(Calendar.HOUR_OF_DAY); - int minute = calendar.get(Calendar.MINUTE); - int second = calendar.get(Calendar.SECOND); - return new File(Minecraft.getInstance().gameDirectory, String.format("screenshots/%04d-%02d-%02d_%02d.%02d.%02d-blazemap-export.png", year, month, day, hour, minute, second)); + return new File(Minecraft.getInstance().gameDirectory, String.format("screenshots/%s-blazemap-export.png", Helpers.getISO8601('-', '.', '.'))); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/AerialViewMapType.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/AerialViewMapType.java index 6c4a5e0e..2a0b4574 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/AerialViewMapType.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/AerialViewMapType.java @@ -1,8 +1,9 @@ package com.eerussianguy.blazemap.feature.mapping; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.maps.MapType; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.lib.Helpers; public class AerialViewMapType extends MapType { @@ -10,7 +11,7 @@ public AerialViewMapType() { super( BlazeMapReferences.MapTypes.AERIAL_VIEW, Helpers.translate("blazemap.aerial_view"), - Helpers.identifier("textures/map_icons/map_aerial.png"), + BlazeMap.resource("textures/map_icons/map_aerial.png"), BlazeMapReferences.Layers.BLOCK_COLOR, BlazeMapReferences.Layers.TERRAIN_SLOPE diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorCollector.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorCollector.java index 7ed85906..a377e4ac 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorCollector.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorCollector.java @@ -1,18 +1,42 @@ package com.eerussianguy.blazemap.feature.mapping; +import java.util.function.IntFunction; + +import java.util.Queue; +import java.util.LinkedList; +import java.util.HashMap; + import net.minecraft.client.Minecraft; import net.minecraft.client.color.block.BlockColors; +import net.minecraft.client.renderer.block.BlockModelShaper; +import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos.MutableBlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.KelpBlock; +import net.minecraft.world.level.block.KelpPlantBlock; +import net.minecraft.world.level.block.SeagrassBlock; +import net.minecraft.world.level.block.TallSeagrassBlock; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.material.MaterialColor; +import net.minecraftforge.client.model.data.EmptyModelData; + import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.BlockColorMD; import com.eerussianguy.blazemap.api.pipeline.ClientOnlyCollector; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Transparency; +import com.eerussianguy.blazemap.lib.Transparency.TransparencyState; +import com.eerussianguy.blazemap.lib.Transparency.BlockComposition; +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; public class BlockColorCollector extends ClientOnlyCollector { + private static final int TINTED_FLAG = 0xFA000000; + private static final Object2IntArrayMap colors = new Object2IntArrayMap<>(); + protected static final HashMap darknessPointCache = new HashMap(); public BlockColorCollector() { super( @@ -25,38 +49,293 @@ public BlockColorCollector() { public BlockColorMD collect(Level level, int minX, int minZ, int maxX, int maxZ) { final int[][] colors = new int[16][16]; final BlockColors blockColors = Minecraft.getInstance().getBlockColors(); - final MutableBlockPos colorPOS = new MutableBlockPos(); + final MutableBlockPos blockPos = new MutableBlockPos(); + + // Reusable arrays + float[] arr1 = new float[4]; + float[] arr2 = new float[4]; + + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + int color = getColorAtMapPixel(level, blockColors, blockPos, minX + x, minZ + z, arr1, arr2); + + if(color > 0) { + colors[z][x] = color; + } + } + } + + return new BlockColorMD(colors); + } + + + protected int getColorAtMapPixel(Level level, BlockColors blockColors, MutableBlockPos blockPos, int x, int z, float[] arr1, float[] arr2) { + int color = 0; + float[] argb = arr1; + // Extra arrays to reuse the same memory addresses for GC's sake + float[] spareArray = arr2; + float[] tmpArray; + + Queue transparentBlocks = new LinkedList(); + + for (int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z); + y > level.getMinBuildHeight(); + y--) { + blockPos.set(x, y, z); + final BlockState state = level.getBlockState(blockPos); + + if (state.isAir()) { + continue; + } + + BlockColor processedBlock = new BlockColor(state, level, blockPos, blockColors, transparentBlocks.isEmpty(), argb, spareArray); + + // TODO: See if this inequality is the cause of the transparency bug + if (processedBlock.totalColor <= 0) { + continue; + } + + if (processedBlock.getTransparencyState() != TransparencyState.OPAQUE) { + transparentBlocks.add(processedBlock); + continue; + } + + // Hasn't met any of the conditions to continue checking the blocks under it, so finalise and break + color = processedBlock.totalColor; + break; + } - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, minX + x, minZ + z); + if (transparentBlocks.size() > 0) { + // TODO: Forgot to change depth while iterating through block column. + // Will require re-tuning colours after fixing. + int depth = transparentBlocks.size(); + argb = transparentBlocks.poll().argb(argb); - int color = 0; - while(color == 0 && y > level.getMinBuildHeight()) { - colorPOS.set(x + minX, y, z + minZ); - final BlockState state = level.getBlockState(colorPOS); + // Top layer must be minimum this colour as it should be the easiest to see. + // Represents extra sunlight reflecting off the surface + argb[0] = Math.max(0.5f, argb[0]); - if (state.isAir()) { - y--; - continue; + while (transparentBlocks.size() > 0) { + BlockColor transparentBlock = transparentBlocks.poll(); + tmpArray = Colors.filterARGB(argb, transparentBlock.argb(spareArray), depth); + spareArray = argb; + argb = tmpArray; + } + + // The shade on the solid block at the bottom of the ocean + color = Colors.recomposeRGBA(Colors.filterARGB(new float[] {0,0,0,0}, Colors.decomposeRGBA(color, spareArray), depth)); + + int finalColor = Colors.recomposeRGBA(argb); + color = Colors.interpolate( + color, 0, + finalColor, 1, + Math.max(0.75f, argb[0]) // It looks silly on thinner layers if the final opacity < 0.75 + ) & 0x00FFFFFF; + } + + return color; + } + + + /** + * These blocks don't return accurate colours using the other methods, + * so unfortunately need to set a colour manually + */ + protected static int handleSpecialCases(BlockState state) { + var block = state.getBlock(); + + // By default, the colour returned for seagrass is purple, so replacing with a green picked from + // its texture + if (block instanceof SeagrassBlock || block instanceof TallSeagrassBlock || block instanceof KelpPlantBlock || block instanceof KelpBlock) { + return 0x215800; + } + + return 0; + } + + protected static int getColorAtPos(Level level, BlockColors blockColors, BlockState state, BlockPos blockPos) { + // int color = handleSpecialCases(state); + int color; + + // Get color from texture + if(state.is(BlockTags.FLOWERS)) { + color = getBestTexturePixel(level, state, null, BlockColorCollector::avoidGreen); + } else { + color = getAverageTextureColor(level, state, Direction.UP); + } + + if((color & Colors.ALPHA) == TINTED_FLAG) { + color = Colors.multiplyRGB(color, blockColors.getColor(state, level, blockPos, 0)); + } + + // Fallback 1: get block tint + if(color == 0) { + color = blockColors.getColor(state, level, blockPos, 0); + } + + // Fallback 2: get block map color + if(color <= 0) { + MaterialColor mapColor = state.getMapColor(level, blockPos); + if(mapColor != MaterialColor.NONE) { + color = mapColor.col; + } + } + + return color; + } + + private static int getAverageTextureColor(Level level, BlockState state, Direction direction) { + return colors.computeIfAbsent(state, $ -> { + var mc = Minecraft.getInstance(); + var model = mc.getModelManager().getModel(BlockModelShaper.stateToModelLocation(state)); + var quads = model.getQuads(state, direction, level.getRandom(), EmptyModelData.INSTANCE); + + int flag = 0; + int r = 0, g = 0, b = 0, total = 0; + + for(var quad : quads) { + if(quad.isTinted()) { + flag = TINTED_FLAG; + } + var texture = quad.getSprite(); + int w = texture.getWidth(), h = texture.getHeight(); + for(int x = 0; x < w; x++) { + for(int y = 0; y < h; y++) { + var pixel = Colors.decomposeRGBA(texture.getPixelRGBA(0, x, y)); + float alpha = pixel[0]; + if(alpha < 0.05F) continue; + r += (255 * pixel[3] * alpha); + g += (255 * pixel[2] * alpha); + b += (255 * pixel[1] * alpha); + total++; } + } + } + + // prevent dumb math and division by zero early + if(total == 0) return 0; + + r /= total; + g /= total; + b /= total; + + return flag | (r << 16) | (g << 8) | b; + }); + } + + private static int getBestTexturePixel(Level level, BlockState state, Direction direction, IntFunction fitness) { + return colors.computeIfAbsent(state, $ -> { + var mc = Minecraft.getInstance(); + var model = mc.getModelManager().getModel(BlockModelShaper.stateToModelLocation(state)); + var quads = model.getQuads(state, direction, level.getRandom(), EmptyModelData.INSTANCE); + int pixel = 0, best = Integer.MIN_VALUE; - color = blockColors.getColor(state, level, colorPOS, 0); - if(color <= 0) { - MaterialColor mapColor = state.getMapColor(level, colorPOS); - if(mapColor != MaterialColor.NONE) { - color = mapColor.col; + for(var quad : quads) { + var texture = quad.getSprite(); + int w = texture.getWidth(), h = texture.getHeight(); + for(int x = 0; x < w; x++) { + for(int y = 0; y < h; y++) { + int color = Colors.abgr(texture.getPixelRGBA(0, x, y)); + int score = fitness.apply(color); + if(score > best) { + best = score; + pixel = color; } } - - y--; } + } - if(color != 0 && color != -1) { - colors[x][z] = color; - } + return pixel; + }); + } + + private static int avoidGreen(int pixel) { + var channels = Colors.decomposeRGBA(pixel); + float a = channels[0]; + int r = (int) (channels[1] * 1000); + int g = (int) (channels[2] * 1000); + int b = (int) (channels[3] * 1000); + return (int) (a * (r + 1000-g + b)); + } + + + public static class BlockColor { + private final BlockComposition blockComposition; + protected final int totalColor; + + private BlockColor(BlockState state, Level level, BlockPos pos, BlockColors blockColors, boolean isSurfaceBlock) { + this(state, level, pos, blockColors, isSurfaceBlock, null, null); + } + + private BlockColor(BlockState state, Level level, BlockPos pos, BlockColors blockColors, boolean isSurfaceBlock, float[] arr1, float[] arr2) { + this.blockComposition = Transparency.getBlockComposition(state, level, pos); + + // Check if we have reusable arrays and create new ones if not + if (arr1 == null || arr2 == null) { + arr1 = new float[4]; + arr2 = new float[4]; + } + + // Get and mix the colours based on the appropriate mixing scheme + switch (blockComposition.compositionState) { + case BLOCK: + case NON_FULL_BLOCK: + // Normal block conditions + this.totalColor = getColorAtPos(level, blockColors, state, pos); + break; + + case FLUID: + // Just a fluid + this.totalColor = getColorAtPos(level, blockColors, state, pos); + break; + + case FLUIDLOGGED_BLOCK: + case FLUIDLOGGED_NON_FULL: + // Fluidlogged block + int blockColor = getColorAtPos(level, blockColors, state, pos); + + BlockState equivalentFluidBlock = state.getFluidState().createLegacyBlock(); + int fluidColor = getColorAtPos(level, blockColors, equivalentFluidBlock, pos); + + float[] blockArgb = argb(blockColor, blockComposition.blockTransparencyLevel.opacity, arr1); + float[] fluidArgb = argb(fluidColor, blockComposition.fluidTransparencyLevel.opacity, arr2); + + if (isSurfaceBlock) { + // Baseline opacity so thin fluids can still be seen + // Represents extra sunlight reflecting off the surface + fluidArgb[0] = Math.max(0.5f, fluidArgb[0]); + } + + float[] totalArgb = Colors.filterARGB(fluidArgb, blockArgb, 0); + this.totalColor = Colors.recomposeRGBA(totalArgb) & 0x00FFFFFF; + break; + + default: + // Zero opacity + this.totalColor = 0x00000000; } } - return new BlockColorMD(colors); + + public float[] argb() { + return argb(totalColor, blockComposition.totalTransparencyLevel.opacity); + } + public float[] argb(float[] arr) { + return argb(totalColor, blockComposition.totalTransparencyLevel.opacity, arr); + } + + protected float[] argb(int color, float opacity) { + float[] argb = Colors.decomposeRGBA(color); + argb[0] = opacity; + return argb; + } + protected float[] argb(int color, float opacity, float[] arr) { + float[] argb = Colors.decomposeRGBA(color, arr); + argb[0] = opacity; + return argb; + } + + public TransparencyState getTransparencyState() { + return blockComposition.getTransparencyState(); + } } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorLayer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorLayer.java index 8b5bf3b5..be943e6b 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorLayer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorLayer.java @@ -1,14 +1,14 @@ package com.eerussianguy.blazemap.feature.mapping; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.BlockColorMD; -import com.eerussianguy.blazemap.api.builtin.WaterLevelMD; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.TileResolution; import com.eerussianguy.blazemap.api.util.ArrayAggregator; -import com.eerussianguy.blazemap.api.util.IDataSource; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.api.util.DataSource; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.platform.NativeImage; public class BlockColorLayer extends Layer { @@ -17,25 +17,23 @@ public BlockColorLayer() { super( BlazeMapReferences.Layers.BLOCK_COLOR, Helpers.translate("blazemap.block_color"), + BlazeMap.resource("textures/map_icons/layer_block_color.png"), + true, - BlazeMapReferences.MasterData.BLOCK_COLOR, - BlazeMapReferences.MasterData.WATER_LEVEL + BlazeMapReferences.MasterData.BLOCK_COLOR ); } @Override - public boolean renderTile(NativeImage tile, TileResolution resolution, IDataSource data, int xGridOffset, int zGridOffset) { + public boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset) { BlockColorMD blocks = (BlockColorMD) data.get(BlazeMapReferences.MasterData.BLOCK_COLOR); if(blocks == null) return false; int[][] blockColors = blocks.colors; - int[][] depths = ((WaterLevelMD) data.get(BlazeMapReferences.MasterData.WATER_LEVEL)).level; foreachPixel(resolution, (x, z) -> { - int depth = ArrayAggregator.avg(relevantData(resolution, x, z, depths)); int color = ArrayAggregator.avgColor(relevantData(resolution, x, z, blockColors)); - float point = ((float) Math.min(depth, 30)) / 50F; - tile.setPixelRGBA(x, z, Colors.interpolate(Colors.abgr(OPAQUE | color), 0, OPAQUE, 1, point)); + tile.setPixelRGBA(x, z, Colors.abgr(OPAQUE | color)); }); return true; diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorSerializer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorSerializer.java index cb62ec7c..d39fe625 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorSerializer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/BlockColorSerializer.java @@ -19,9 +19,9 @@ public BlazeRegistry.Key getID() { @Override public void serialize(MinecraftStreams.Output stream, BlockColorMD datum) throws IOException { - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - stream.writeInt(datum.colors[x][z]); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + stream.writeInt(datum.colors[z][x]); } } } @@ -30,9 +30,9 @@ public void serialize(MinecraftStreams.Output stream, BlockColorMD datum) throws public BlockColorMD deserialize(MinecraftStreams.Input stream) throws IOException { int[][] colors = new int[16][16]; - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - colors[x][z] = stream.readInt(); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + colors[z][x] = stream.readInt(); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherCollector.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherCollector.java index bde0fb26..009bdd5b 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherCollector.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherCollector.java @@ -21,8 +21,8 @@ public NetherCollector() { public TerrainHeightMD collect(Level level, int minX, int minZ, int maxX, int maxZ) { final int[][] heightmap = new int[16][16]; - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { int height = 110; while(isNotAir(level, minX + x, height - 1, minZ + z)) { height--; @@ -34,7 +34,7 @@ public TerrainHeightMD collect(Level level, int minX, int minZ, int maxX, int ma if(height <= level.getMinBuildHeight()) break; } } - heightmap[x][z] = height; + heightmap[z][x] = height; } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherLayer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherLayer.java index ab495a96..f3957ea8 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherLayer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherLayer.java @@ -5,14 +5,15 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.Level; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.TerrainHeightMD; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.TileResolution; import com.eerussianguy.blazemap.api.util.ArrayAggregator; -import com.eerussianguy.blazemap.api.util.IDataSource; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.api.util.DataSource; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.platform.NativeImage; public class NetherLayer extends Layer { @@ -20,6 +21,8 @@ public NetherLayer() { super( BlazeMapReferences.Layers.NETHER, Helpers.translate("blazemap.nether_terrain"), + BlazeMap.resource("textures/map_icons/layer_nether.png"), + true, BlazeMapReferences.MasterData.NETHER ); @@ -51,7 +54,7 @@ public boolean shouldRenderInDimension(ResourceKey dimension) { } @Override - public boolean renderTile(NativeImage tile, TileResolution resolution, IDataSource data, int xGridOffset, int zGridOffset) { + public boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset) { TerrainHeightMD terrain = (TerrainHeightMD) data.get(BlazeMapReferences.MasterData.NETHER); float down = -1.0F / ((float) terrain.sea - terrain.minY); float up = 1.0F / ((float) terrain.maxY - terrain.sea); diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherMapType.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherMapType.java index 96c7cd28..0377173f 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherMapType.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/NetherMapType.java @@ -3,14 +3,15 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.Level; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.maps.MapType; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.lib.Helpers; public class NetherMapType extends MapType { public NetherMapType() { - super(BlazeMapReferences.MapTypes.NETHER, Helpers.translate("blazemap.nether"), Helpers.identifier("textures/map_icons/map_nether.png"), BlazeMapReferences.Layers.NETHER); + super(BlazeMapReferences.MapTypes.NETHER, Helpers.translate("blazemap.nether"), BlazeMap.resource("textures/map_icons/map_nether.png"), BlazeMapReferences.Layers.NETHER); } @Override diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightCollector.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightCollector.java index c7412125..c4a00ef2 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightCollector.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightCollector.java @@ -1,9 +1,6 @@ package com.eerussianguy.blazemap.feature.mapping; -import net.minecraft.tags.BlockTags; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.levelgen.Heightmap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.TerrainHeightMD; @@ -20,31 +17,46 @@ public TerrainHeightCollector() { @Override public TerrainHeightMD collect(Level level, int minX, int minZ, int maxX, int maxZ) { - - final int[][] heightmap = new int[16][16]; - - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - int height = level.getHeight(Heightmap.Types.MOTION_BLOCKING, minX + x, minZ + z); - boolean foundLeaves = false; - while(isLeavesOrReplaceable(level, minX + x, height - 1, minZ + z)) { - height--; - if(height <= level.getMinBuildHeight()) break; - foundLeaves = true; - } - while(foundLeaves && isSkippableAfterLeaves(level, minX + x, height - 1, minZ + z)) { - height--; - if(height <= level.getMinBuildHeight()) break; - } - heightmap[x][z] = height; + final int[][] heightmapTerrain = new int[16][16]; + // final int[][] heightmapSurface = new int[16][16]; + // final int[][] heightmapOpaque = new int[16][16]; + // final float[][] heightmapAttenuation = new float[16][16]; + + final int minBuildHeight = level.getMinBuildHeight(); + + int blockX; + int blockZ; + + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + blockX = x + minX; + blockZ = z + minZ; + /** + * Collect heights of the highest and lowest block that can be seen. + * (Primarily for shadow implementation) + */ + // TODO: Waiting until Transformer improvements have been made in BME-198 before implementing + + /** + * Now collect base terrain height. + * This ignores non-terrain blocks such as trees and other plantlife + */ + int height = findSurfaceBelowVegetation(level, blockX, blockZ, false); + + // Note: The + 1 here is for legacy reasons. Will make it somebody else's decision + // wether or not to remove it and possibly make other visual adjustments instead. + heightmapTerrain[z][x] = height + 1; } } - return new TerrainHeightMD(BlazeMapReferences.MasterData.TERRAIN_HEIGHT, level.getMinBuildHeight(), level.getMaxBuildHeight(), level.getHeight(), level.getSeaLevel(), heightmap); - } - protected static boolean isSkippableAfterLeaves(Level level, int x, int y, int z) { - BlockState state = level.getBlockState(POS.set(x, y, z)); - return state.is(BlockTags.LEAVES) || state.isAir() || state.is(BlockTags.LOGS) || state.getMaterial().isReplaceable(); + return new TerrainHeightMD( + BlazeMapReferences.MasterData.TERRAIN_HEIGHT, + minBuildHeight, + level.getMaxBuildHeight(), + level.getHeight(), + level.getSeaLevel(), + heightmapTerrain + ); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLayer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLayer.java index 2427aa3a..a41c78f0 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLayer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLayer.java @@ -2,17 +2,17 @@ import java.awt.*; -import net.minecraft.client.gui.components.Widget; - +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.TerrainHeightMD; import com.eerussianguy.blazemap.api.builtin.WaterLevelMD; import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.Renderable; import com.eerussianguy.blazemap.api.maps.TileResolution; import com.eerussianguy.blazemap.api.util.ArrayAggregator; -import com.eerussianguy.blazemap.api.util.IDataSource; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.api.util.DataSource; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.platform.NativeImage; public class TerrainHeightLayer extends Layer { @@ -21,6 +21,8 @@ public TerrainHeightLayer() { super( BlazeMapReferences.Layers.TERRAIN_HEIGHT, Helpers.translate("blazemap.terrain_height"), + BlazeMap.resource("textures/map_icons/layer_terrain_height.png"), + true, BlazeMapReferences.MasterData.TERRAIN_HEIGHT, BlazeMapReferences.MasterData.WATER_LEVEL @@ -53,7 +55,7 @@ private enum Gradient { } @Override - public boolean renderTile(NativeImage tile, TileResolution resolution, IDataSource data, int xGridOffset, int zGridOffset) { + public boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset) { TerrainHeightMD terrain = (TerrainHeightMD) data.get(BlazeMapReferences.MasterData.TERRAIN_HEIGHT); WaterLevelMD water = (WaterLevelMD) data.get(BlazeMapReferences.MasterData.WATER_LEVEL); float down = -1.0F / ((float) terrain.sea - terrain.minY); @@ -69,8 +71,8 @@ public boolean renderTile(NativeImage tile, TileResolution resolution, IDataSour } @Override - public Widget getLegendWidget() { - return new TerrainHeightLegendWidget(); + public Renderable getLegendWidget() { + return new TerrainHeightLegend(); } private static final int DOWNSIZE = 4; diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLegendWidget.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLegend.java similarity index 60% rename from src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLegendWidget.java rename to src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLegend.java index 81b54bb7..39d2cf08 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLegendWidget.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightLegend.java @@ -1,7 +1,6 @@ package com.eerussianguy.blazemap.feature.mapping; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.Widget; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; @@ -9,14 +8,20 @@ import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.resources.ResourceLocation; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; -import com.eerussianguy.blazemap.util.RenderHelper; +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.Tesselator; -public class TerrainHeightLegendWidget implements Widget { +public class TerrainHeightLegend extends BaseComponent { + private static final int BORDER = 4; + private static final int GRADIENT_WIDTH = 10; + private static final int LABEL_STEP = 64; + private static final float TEXT_SCALE = 0.5F; + private static NativeImage legend; private static RenderType type; private static int min; @@ -31,39 +36,41 @@ private static RenderType getLegend() { max = level.getMaxBuildHeight(); legend = TerrainHeightLayer.getLegend(min, sea, max); DynamicTexture texture = new DynamicTexture(legend); - ResourceLocation path = Helpers.identifier("dynamic/legend/terrain_height"); + ResourceLocation path = BlazeMap.resource("dynamic/legend/terrain_height"); mc.getTextureManager().register(path, texture); type = RenderType.text(path); } return type; } - @Override - public void render(PoseStack stack, int i, int j, float k) { + public TerrainHeightLegend() { if(legend == null) getLegend(); + setSize(10 + GRADIENT_WIDTH + BORDER * 2 , legend.getHeight() + BORDER * 2); + } - int height = legend.getHeight(); - stack.translate(-28, -(height + 8), 0); + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + if(legend == null) getLegend(); var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); - RenderHelper.fillRect(buffers, stack.last().pose(), 28, height + 8, Colors.WIDGET_BACKGROUND); + RenderHelper.fillRect(buffers, stack.last().pose(), getWidth(), getHeight(), Colors.WIDGET_BACKGROUND); stack.pushPose(); - stack.translate(16, 4, 0); - RenderHelper.drawQuad(buffers.getBuffer(getLegend()), stack.last().pose(), 10, height); + stack.translate(16, BORDER, 0); + RenderHelper.drawQuad(buffers.getBuffer(getLegend()), stack.last().pose(), GRADIENT_WIDTH, legend.getHeight()); stack.popPose(); var font = Minecraft.getInstance().font; stack.pushPose(); stack.translate(0, 2, 0); - stack.scale(0.5F, 0.5F, 1); - for(int y = max; y >= min; y -= 64) { + stack.scale(TEXT_SCALE, TEXT_SCALE, 1); + for(int y = max; y >= min; y -= LABEL_STEP) { String label = String.valueOf(y); stack.pushPose(); - stack.translate(28 - font.width(label), 0, 0); + stack.translate(getWidth() - font.width(label), 0, 0); font.drawInBatch(label, 0, 0, Colors.WHITE, false, stack.last().pose(), buffers, false, 0, LightTexture.FULL_BRIGHT); stack.popPose(); - stack.translate(0, 32, 0); + stack.translate(0, LABEL_STEP * TEXT_SCALE, 0); } stack.popPose(); diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightSerializer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightSerializer.java index 5882e570..40485476 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightSerializer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainHeightSerializer.java @@ -24,10 +24,9 @@ public void serialize(MinecraftStreams.Output stream, TerrainHeightMD terrain) t stream.writeShort(terrain.height); stream.writeShort(terrain.sea); - - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - stream.writeShort(terrain.heightmap[x][z]); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + stream.writeShort(terrain.heightmap[z][x]); } } } @@ -42,9 +41,9 @@ public TerrainHeightMD deserialize(MinecraftStreams.Input stream) throws IOExcep int[][] heightmap = new int[16][16]; - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - heightmap[x][z] = stream.readShort(); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + heightmap[z][x] = stream.readShort(); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainIsolinesLayer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainIsolinesLayer.java index b56e9f97..1aa53cef 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainIsolinesLayer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainIsolinesLayer.java @@ -1,13 +1,14 @@ package com.eerussianguy.blazemap.feature.mapping; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.TerrainHeightMD; import com.eerussianguy.blazemap.api.builtin.WaterLevelMD; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.TileResolution; import com.eerussianguy.blazemap.api.util.ArrayAggregator; -import com.eerussianguy.blazemap.api.util.IDataSource; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.api.util.DataSource; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.platform.NativeImage; public class TerrainIsolinesLayer extends Layer { @@ -32,7 +33,8 @@ public TerrainIsolinesLayer() { super( BlazeMapReferences.Layers.TERRAIN_ISOLINES, Helpers.translate("blazemap.terrain_isolines"), - Helpers.identifier("textures/map_icons/layer_terrain_isolines.png"), + BlazeMap.resource("textures/map_icons/layer_terrain_isolines.png"), + false, BlazeMapReferences.MasterData.TERRAIN_HEIGHT, BlazeMapReferences.MasterData.WATER_LEVEL @@ -40,7 +42,7 @@ public TerrainIsolinesLayer() { } @Override - public boolean renderTile(NativeImage tile, TileResolution resolution, IDataSource data, int xGridOffset, int zGridOffset) { + public boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset) { TerrainHeightMD terrain = (TerrainHeightMD) data.get(BlazeMapReferences.MasterData.TERRAIN_HEIGHT); WaterLevelMD water = (WaterLevelMD) data.get(BlazeMapReferences.MasterData.WATER_LEVEL); diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeCollector.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeCollector.java index b4b0bd18..a073cc54 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeCollector.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeCollector.java @@ -3,13 +3,18 @@ import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.TerrainSlopeMD; import com.eerussianguy.blazemap.api.pipeline.Collector; +import com.eerussianguy.blazemap.lib.Transparency; +import com.eerussianguy.blazemap.lib.Transparency.CompositionState; +import com.eerussianguy.blazemap.lib.Transparency.TransparencyState; +import com.eerussianguy.blazemap.lib.Transparency.BlockComposition; -import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos.MutableBlockPos; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.levelgen.Heightmap; public class TerrainSlopeCollector extends Collector { - public TerrainSlopeCollector() { super( BlazeMapReferences.Collectors.TERRAIN_SLOPE, @@ -17,21 +22,145 @@ public TerrainSlopeCollector() { ); } + record Height(int highestHeight, int lowestHeight, float totalOpacity) {} + + // For when a one-off BlockPos is needed, to prevent creating more new objects than necessary + // ThreadLocal to prevent accidental "cross-contamination" + private static ThreadLocal miscReusablePos = ThreadLocal.withInitial(() -> new MutableBlockPos(0, 0, 0)); + private static float OPACITY_MIN = 0.25f; + @Override public TerrainSlopeMD collect(Level level, int minX, int minZ, int maxX, int maxZ) { - final float[][] slopemap = new float[16][16]; + // For the 16 x 16 chunk + 2 blocks on either side + // TODO: Once BME-198 completed, can remove calls into neighbouring chunks + final Height[][] heightmap = new Height[20][20]; + final int[][] highestHeightmap = new int[20][20]; + final int[][] lowestHeightmap = new int[20][20]; + final float[][] opacityMap = new float[20][20]; - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - slopemap[x][z] = getSlopeGradient(level, minX + x, minZ + z); + for(int z = -2; z < 18; z++) { + for(int x = -2; x < 18; x++) { + heightmap[z + 2][x + 2] = getHeight(level, minX + x, minZ + z, POS); + highestHeightmap[z + 2][x + 2] = heightmap[z + 2][x + 2].highestHeight; + lowestHeightmap[z + 2][x + 2] = heightmap[z + 2][x + 2].lowestHeight; + opacityMap[z + 2][x + 2] = heightmap[z + 2][x + 2].totalOpacity; } } + // The below should be possible to be moved off-thread to a Transformer later + final float[][] slopemap = processSlopeData(highestHeightmap, lowestHeightmap, opacityMap, level, minX, minZ); + return new TerrainSlopeMD(slopemap); } - protected static float getSlopeGradient(Level level, int x, int z) { - int height = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z); + + protected static Height getHeight(Level level, int x, int z, MutableBlockPos blockPos) { + int height = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1; + int highestHeight = height; + + blockPos.set(x, height, z); + boolean isTransparent = false; + float transparency = 1; + + BlockState state = level.getBlockState(blockPos); + BlockComposition blockComposition = Transparency.getBlockComposition(state, level, blockPos); + + while (height > level.getMinBuildHeight() + && ( + TransparencyState.isAtLeastAsTransparentAs( + blockComposition.getTransparencyState(), + TransparencyState.SEMI_TRANSPARENT + ) + // TODO: Make work better with bamboo + || blockComposition.getBlockCompositionState() == CompositionState.NON_FULL_BLOCK + ) + ) { + isTransparent = true; + transparency = transparency * blockComposition.getTransparencyState().transparency; + + height--; + state = level.getBlockState(blockPos.move(Direction.DOWN)); + blockComposition = Transparency.getBlockComposition(state, level, blockPos); + } + + float opacity = isTransparent ? 1 - transparency : 1; + + return new Height(highestHeight, height, opacity); + } + + + protected static float[][] processSlopeData( + int[][] highestHeightmap, + int[][] lowestHeightmap, + float[][] opacityMap, + Level level, + int minX, int minZ + ) { + final float[][] slopemap = new float[16][16]; + + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + int xOffset = x + 2; + int zOffset = z + 2; + + float topSlope = getSlopeGradient( + highestHeightmap, + highestHeightmap, + opacityMap, + level, + x, z, + minX, minZ + ); + + if (highestHeightmap[zOffset][xOffset] != lowestHeightmap[zOffset][xOffset]) { + float baseSlope = getSlopeGradient( + lowestHeightmap, + lowestHeightmap, + null, + level, + x, z, + minX, minZ + ); + + int depth = highestHeightmap[zOffset][xOffset] - lowestHeightmap[zOffset][xOffset]; + + // Note: 1/(16^2) = 0.00390625 + float point = Math.max(1 - 0.00390625f * (depth * depth), 0); + + // Direct attenuation from transparent object above + baseSlope = baseSlope * (1 - Math.max(opacityMap[zOffset][xOffset], OPACITY_MIN)); + // Extra attenuation from light scatter within transparent blocks + baseSlope = baseSlope > 0 ? + Math.min(Math.min(baseSlope, point), 0.25f) : // shadow + Math.max(Math.max(baseSlope, -(point * point)), -0.3125f); // sunlight (Note: -0.3125 = -5/16) + + // Sunlight and shadows on a transparent surface aren't as strong as on an opaque one + topSlope = topSlope * Math.max(opacityMap[zOffset][xOffset], OPACITY_MIN); + + // Combine the two cases + slopemap[z][x] = baseSlope + topSlope; + + } else { + slopemap[z][x] = topSlope; + } + + } + } + + return slopemap; + } + + + protected static float getSlopeGradient( + int[][] thisHeightmap, + int[][] adjacentHeightmap, + float[][] opacityMap, + Level level, + int x, int z, + int minX, int minZ + ) { + int xOffset = x + 2; + int zOffset = z + 2; float nearSlopeTotal = 0; float nearSlopeCount = 0; @@ -41,8 +170,8 @@ protected static float getSlopeGradient(Level level, int x, int z) { // Slope direction is relative to North West/top left of the map as that's the direction our // "sunlight" is going to be coming from. +x == East, +z == South. // Positive values means in shadow, negative values means in light. - for (int dx = -2; dx <= 0; dx++) { - for (int dz = -2; dz <= 0; dz++) { + for (int dz = -2; dz <= 0; dz++) { + for (int dx = -2; dx <= 0; dx++) { if (dx == 0 && dz == 0) { continue; } else if (dx == -2 && dz == -2) { @@ -51,27 +180,49 @@ protected static float getSlopeGradient(Level level, int x, int z) { if (dx >= -1 && dz >= -1) { // Adjacent block - int nearSlope = getRelativeSlope(level, x, z, height, dx, dz, true); + int nearSlope = getRelativeSlope( + thisHeightmap[zOffset][xOffset], + adjacentHeightmap[zOffset + dz][xOffset + dx], + adjacentHeightmap[zOffset - dz][xOffset - dx], + level, + x + minX, z + minZ, + dx, dz, + true); if (nearSlope < 0) { nearSlopeTotal += nearSlope; nearSlopeCount += 1 - (0.4 * nearSlopeCount); } else if (nearSlope > 0) { // Shadows are weighted more heavily than sunlight - nearSlopeTotal += 4 * nearSlope; + nearSlopeTotal += 4 * nearSlope * ( + opacityMap != null ? + Math.max(opacityMap[zOffset + dz][xOffset + dx], OPACITY_MIN) : + 1 + ); nearSlopeCount += 4 - (0.5 * nearSlopeCount); } } else if (dx >= -2 && dz >= -2) { // Two blocks away - int farSlope = getRelativeSlope(level, x, z, height, dx, dz, false); + int farSlope = getRelativeSlope( + thisHeightmap[zOffset][xOffset], + adjacentHeightmap[zOffset + dz][xOffset + dx], + adjacentHeightmap[zOffset - dz][xOffset - dx], + level, + x + minX, z + minZ, + dx, dz, + false); if (farSlope < -2) { farSlopeTotal += farSlope; farSlopeCount += 1 - (0.4 * farSlopeCount); } else if (farSlope > 2) { // Shadows are weighted more heavily than sunlight - farSlopeTotal += 4 * farSlope; + farSlopeTotal += 4 * farSlope * ( + opacityMap != null ? + Math.max(opacityMap[zOffset + dz][xOffset + dx], OPACITY_MIN) : + 1 + ); farSlopeCount += 4 - (0.5 * farSlopeCount); } } @@ -82,12 +233,19 @@ protected static float getSlopeGradient(Level level, int x, int z) { (farSlopeCount != 0 ? farSlopeTotal / farSlopeCount : 0) / 2; } - protected static int getRelativeSlope(Level level, int x, int z, int height, int dx, int dz, boolean isPrimaryShadow) { - int adjacentBlockHeight = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x + dx, z + dz); - int relativeSlope = adjacentBlockHeight - height; + protected static int getRelativeSlope( + int blockHeight, + int adjacentHeight, + int oppositeHeight, + Level level, + int x, int z, + int dx, int dz, + boolean isPrimaryShadow + ) { + int relativeSlope = adjacentHeight - blockHeight; - if (adjacentBlockHeight <= -64 && level.getBlockState(new BlockPos(x + dx, adjacentBlockHeight, z + dz)).isAir()) { + if (adjacentHeight <= level.getMinBuildHeight() && level.getBlockState(miscReusablePos.get().set(x + dx, adjacentHeight, z + dz)).isAir()) { // This block is in an unloaded chunk and can't be processed until BME-47 is dealt with. // (Alternatively, somebody's broken through to the void, but that's their own fault!) return 0; @@ -100,16 +258,13 @@ protected static int getRelativeSlope(Level level, int x, int z, int height, int // Add shadow return relativeSlope; - } else { - int oppositeBlockHeight = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x - dx, z - dz); - - if ( - (isPrimaryShadow && oppositeBlockHeight < height) || - (!isPrimaryShadow && oppositeBlockHeight <= height)) { - // At the top of a slope - return 0; - } + } else if ( + (isPrimaryShadow && oppositeHeight < blockHeight) || + (!isPrimaryShadow && oppositeHeight <= blockHeight)) { + // At the top of a slope + return 0; + } else { // Add sunlight return relativeSlope; } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeLayer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeLayer.java index bdca0157..1e88cfa1 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeLayer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeLayer.java @@ -1,13 +1,14 @@ package com.eerussianguy.blazemap.feature.mapping; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.TerrainSlopeMD; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.TileResolution; import com.eerussianguy.blazemap.api.util.ArrayAggregator; -import com.eerussianguy.blazemap.api.util.IDataSource; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.api.util.DataSource; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.platform.NativeImage; public class TerrainSlopeLayer extends Layer { @@ -19,14 +20,15 @@ public TerrainSlopeLayer() { BlazeMapReferences.Layers.TERRAIN_SLOPE, Helpers.translate("blazemap.terrain_slope"), // This should be changed at some point to its own dedicated image - Helpers.identifier("textures/map_icons/layer_terrain_isolines.png"), + BlazeMap.resource("textures/map_icons/layer_terrain_isolines.png"), + false, BlazeMapReferences.MasterData.TERRAIN_SLOPE ); } @Override - public boolean renderTile(NativeImage tile, TileResolution resolution, IDataSource data, int xGridOffset, int zGridOffset) { + public boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset) { TerrainSlopeMD terrain = (TerrainSlopeMD) data.get(BlazeMapReferences.MasterData.TERRAIN_SLOPE); foreachPixel(resolution, (x, z) -> { @@ -43,13 +45,13 @@ private static void paintSlope(NativeImage tile, int x, int z, float slope) { return; } else if (slope > 0) { - float slopeLog = (float)Helpers.clamp(0, (Math.log(slope) * 10), SHADING_RANGE); - int shadow = Colors.interpolate(0x30000000, 0, 0x70000000, SHADING_RANGE, slopeLog); + float slopeLog = (float)Helpers.clamp(-SHADING_RANGE * 0.75f, (Math.log(slope) * 10), SHADING_RANGE); + int shadow = Colors.interpolate(0x00000000, -SHADING_RANGE * 0.75f, 0x70000000, SHADING_RANGE, slopeLog); tile.setPixelRGBA(x, z, shadow); } else { - float slopeLog = (float)Helpers.clamp(0, (Math.log(-slope) * 10), SHADING_RANGE); - int sunlight = Colors.interpolate(0x20FFFFFF, 0, 0x60FFFFFF, SHADING_RANGE, slopeLog); + float slopeLog = (float)Helpers.clamp(-SHADING_RANGE * 0.5f, (Math.log(-slope) * 10), SHADING_RANGE); + int sunlight = Colors.interpolate(0x00FFFFFF, -SHADING_RANGE * 0.5f, 0x60FFFFFF, SHADING_RANGE, slopeLog); tile.setPixelRGBA(x, z, sunlight); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeSerializer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeSerializer.java index dfc166bd..db728520 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeSerializer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TerrainSlopeSerializer.java @@ -11,9 +11,9 @@ public class TerrainSlopeSerializer implements DataType { @Override public void serialize(MinecraftStreams.Output stream, TerrainSlopeMD terrain) throws IOException { - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - stream.writeFloat(terrain.slopemap[x][z]); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + stream.writeFloat(terrain.slopemap[z][x]); } } } @@ -22,9 +22,9 @@ public void serialize(MinecraftStreams.Output stream, TerrainSlopeMD terrain) th public TerrainSlopeMD deserialize(MinecraftStreams.Input stream) throws IOException { float[][] slopemap = new float[16][16]; - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - slopemap[x][z] = stream.readFloat(); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + slopemap[z][x] = stream.readFloat(); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TopographyMapType.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TopographyMapType.java index 9d1ed186..c75c642e 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/TopographyMapType.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/TopographyMapType.java @@ -3,16 +3,17 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.Level; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.maps.MapType; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.lib.Helpers; public class TopographyMapType extends MapType { public TopographyMapType() { super( BlazeMapReferences.MapTypes.TOPOGRAPHY, Helpers.translate("blazemap.topography"), - Helpers.identifier("textures/map_icons/map_topography.png"), + BlazeMap.resource("textures/map_icons/map_topography.png"), BlazeMapReferences.Layers.TERRAIN_HEIGHT, BlazeMapReferences.Layers.TERRAIN_ISOLINES, diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelCollector.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelCollector.java index 1a91001d..7270f37a 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelCollector.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelCollector.java @@ -1,7 +1,6 @@ package com.eerussianguy.blazemap.feature.mapping; import net.minecraft.world.level.Level; -import net.minecraft.world.level.levelgen.Heightmap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.WaterLevelMD; @@ -20,16 +19,24 @@ public WaterLevelCollector() { @Override public WaterLevelMD collect(Level level, int minX, int minZ, int maxX, int maxZ) { + int blockX; + int blockZ; final int[][] water = new int[16][16]; - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - int depth = 0, height = level.getHeight(Heightmap.Types.MOTION_BLOCKING, minX + x, minZ + z) - 1; - while(isWater(level, minX + x, height - depth, minZ + z)) { + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + blockX = x + minX; + blockZ = z + minZ; + + int depth = 0; + int height = findSurfaceBelowVegetation(level, blockX, blockZ, true); + + while(isWater(level, blockX, height - depth, blockZ)) { depth++; if(height - depth < level.getMinBuildHeight()) break; } - water[x][z] = depth; + + water[z][x] = depth; } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelLayer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelLayer.java index 32e01acb..8149dd0f 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelLayer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelLayer.java @@ -1,12 +1,13 @@ package com.eerussianguy.blazemap.feature.mapping; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.builtin.WaterLevelMD; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.TileResolution; import com.eerussianguy.blazemap.api.util.ArrayAggregator; -import com.eerussianguy.blazemap.api.util.IDataSource; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.api.util.DataSource; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.platform.NativeImage; public class WaterLevelLayer extends Layer { @@ -15,14 +16,15 @@ public WaterLevelLayer() { super( BlazeMapReferences.Layers.WATER_LEVEL, Helpers.translate("blazemap.water_depth"), - Helpers.identifier("textures/map_icons/layer_water.png"), + BlazeMap.resource("textures/map_icons/layer_water.png"), + false, BlazeMapReferences.MasterData.WATER_LEVEL ); } @Override - public boolean renderTile(NativeImage tile, TileResolution resolution, IDataSource data, int xGridOffset, int zGridOffset) { + public boolean renderTile(NativeImage tile, TileResolution resolution, DataSource data, int xGridOffset, int zGridOffset) { WaterLevelMD water = (WaterLevelMD) data.get(BlazeMapReferences.MasterData.WATER_LEVEL); foreachPixel(resolution, (x, z) -> { diff --git a/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelSerializer.java b/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelSerializer.java index 0d766c4c..ec786dce 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelSerializer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/mapping/WaterLevelSerializer.java @@ -18,9 +18,9 @@ public BlazeRegistry.Key getID() { public void serialize(MinecraftStreams.Output stream, WaterLevelMD water) throws IOException { stream.writeShort(water.sea); - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - stream.writeShort(water.level[x][z]); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + stream.writeShort(water.level[z][x]); } } } @@ -31,9 +31,9 @@ public WaterLevelMD deserialize(MinecraftStreams.Input stream) throws IOExceptio int[][] level = new int[16][16]; - for(int x = 0; x < 16; x++) { - for(int z = 0; z < 16; z++) { - level[x][z] = stream.readShort(); + for(int z = 0; z < 16; z++) { + for(int x = 0; x < 16; x++) { + level[z][x] = stream.readShort(); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/DefaultObjectRenderer.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/DefaultObjectRenderer.java index 0c3a331a..7155e54e 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/DefaultObjectRenderer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/DefaultObjectRenderer.java @@ -1,19 +1,21 @@ package com.eerussianguy.blazemap.feature.maps; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; import com.eerussianguy.blazemap.api.BlazeMapReferences; import com.eerussianguy.blazemap.api.BlazeRegistry; -import com.eerussianguy.blazemap.api.markers.MapLabel; +import com.eerussianguy.blazemap.api.markers.Marker; import com.eerussianguy.blazemap.api.markers.ObjectRenderer; import com.eerussianguy.blazemap.api.markers.SearchTargeting; -import com.eerussianguy.blazemap.util.RenderHelper; +import com.eerussianguy.blazemap.lib.RenderHelper; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.math.Vector3f; -public class DefaultObjectRenderer implements ObjectRenderer { +public class DefaultObjectRenderer implements ObjectRenderer> { @Override public BlazeRegistry.Key> getID() { @@ -21,15 +23,34 @@ public BlazeRegistry.Key> getID() { } @Override - public void render(MapLabel label, PoseStack stack, MultiBufferSource buffers, double zoom, SearchTargeting search) { - if(!label.getUsesZoom()) { + public void render(Marker marker, PoseStack stack, MultiBufferSource buffers, double zoom, SearchTargeting search) { + // Set appropriate scale for the current zoom level + if(!marker.getUsesZoom()) { stack.scale(1F / (float) zoom, 1F / (float) zoom, 1); } - stack.mulPose(Vector3f.ZP.rotationDegrees(label.getRotation())); - int width = label.getWidth(); - int height = label.getHeight(); + + // Get common marker properties + String name = marker.getName(); + int width = marker.getWidth(); + int height = marker.getHeight(); + int color = marker.getColor(); + + // Render marker name + if(marker.isNameVisible() && name != null) { + float scale = 2; + Minecraft mc = Minecraft.getInstance(); + + stack.pushPose(); + stack.translate(-mc.font.width(name), (10 + (height / scale)), 0); + stack.scale(scale, scale, 0); + mc.font.drawInBatch(name, 0, 0, search.color(color), true, stack.last().pose(), buffers, false, 0, LightTexture.FULL_BRIGHT); + stack.popPose(); + } + + // Render marker icon / texture + stack.mulPose(Vector3f.ZP.rotationDegrees(marker.getRotation())); stack.translate(-width / 2, -height / 2, 0); - VertexConsumer vertices = buffers.getBuffer(RenderType.text(label.getIcon())); - RenderHelper.drawQuad(vertices, stack.last().pose(), (float) width, (float) height, search.color(label.getColor())); + VertexConsumer vertices = buffers.getBuffer(RenderType.text(marker.getIcon())); + RenderHelper.drawQuad(vertices, stack.last().pose(), (float) width, (float) height, search.color(color)); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/IMapHost.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/IMapHost.java deleted file mode 100644 index ba30cad6..00000000 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/IMapHost.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.eerussianguy.blazemap.feature.maps; - -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.network.chat.Component; - -import com.eerussianguy.blazemap.api.BlazeRegistry.Key; -import com.eerussianguy.blazemap.api.maps.Layer; -import com.eerussianguy.blazemap.api.maps.MapType; -import com.mojang.blaze3d.vertex.PoseStack; - -public interface IMapHost { - void drawTooltip(PoseStack stack, Component component, int x, int y); - - boolean isLayerVisible(Key layerID); - - void toggleLayer(Key layerID); - - MapType getMapType(); - - void setMapType(MapType map); - - Iterable getChildren(); -} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/MDInspectorWidget.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/MDInspectorWidget.java index 39be57fb..4611a467 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/MDInspectorWidget.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/MDInspectorWidget.java @@ -11,8 +11,8 @@ import com.eerussianguy.blazemap.api.debug.MDInspectionController; import com.eerussianguy.blazemap.api.pipeline.MasterDatum; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.RenderHelper; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; import com.mojang.blaze3d.platform.Window; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/MapConfigSynchronizer.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/MapConfigSynchronizer.java index b3071aed..c3a01490 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/MapConfigSynchronizer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/MapConfigSynchronizer.java @@ -1,9 +1,11 @@ package com.eerussianguy.blazemap.feature.maps; -import com.eerussianguy.blazemap.config.ClientConfig.MapConfig; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.config.ClientConfig.MapConfig; +import com.eerussianguy.blazemap.engine.render.MapRenderer; public class MapConfigSynchronizer { protected final MapConfig config; @@ -21,6 +23,7 @@ public MapConfigSynchronizer(MapRenderer map, MapConfig config) { public void load() { renderer.setMapType(config.activeMap.get().value()); renderer.setDisabledLayers(config.disabledLayers.get()); + renderer.setDisabledOverlays(config.disabledOverlays.get()); renderer.setZoom(config.zoom.get()); } @@ -42,6 +45,12 @@ public boolean toggleLayer(Key layerID) { return true; } + public boolean toggleOverlay(Key overlayID) { + if(!renderer.toggleOverlay(overlayID)) return false; + config.disabledOverlays.set(renderer.getDisabledOverlays()); + return true; + } + public boolean setZoom(double zoom) { if(!renderer.setZoom(zoom)) return false; config.zoom.set(renderer.getZoom()); diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapConfigSynchronizer.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapConfigSynchronizer.java index ef2cc849..b0158a13 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapConfigSynchronizer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapConfigSynchronizer.java @@ -1,6 +1,7 @@ package com.eerussianguy.blazemap.feature.maps; import com.eerussianguy.blazemap.config.ClientConfig.MinimapConfig; +import com.eerussianguy.blazemap.engine.render.MapRenderer; public class MinimapConfigSynchronizer extends MapConfigSynchronizer { public MinimapConfigSynchronizer(MapRenderer map, MinimapConfig config) { diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapOptionsGui.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapOptionsGui.java index 3a1620b2..9713e65b 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapOptionsGui.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapOptionsGui.java @@ -1,26 +1,29 @@ package com.eerussianguy.blazemap.feature.maps; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.network.chat.Component; import net.minecraft.network.chat.TranslatableComponent; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.Level; +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.__deprecated.BlazeGui; +import com.eerussianguy.blazemap.__deprecated.LayerButton; +import com.eerussianguy.blazemap.__deprecated.MapTypeButton; import com.eerussianguy.blazemap.api.BlazeMapAPI; import com.eerussianguy.blazemap.api.BlazeRegistry.Key; -import com.eerussianguy.blazemap.api.maps.IScreenSkipsMinimap; import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.Overlay; import com.eerussianguy.blazemap.config.BlazeMapConfig; import com.eerussianguy.blazemap.config.ClientConfig; import com.eerussianguy.blazemap.config.MinimapConfigFacade; -import com.eerussianguy.blazemap.gui.BlazeGui; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.engine.render.MapRenderer; +import com.eerussianguy.blazemap.feature.maps.ui.MapHost; +import com.eerussianguy.blazemap.lib.Helpers; import com.mojang.blaze3d.vertex.PoseStack; -public class MinimapOptionsGui extends BlazeGui implements IScreenSkipsMinimap, IMapHost { +public class MinimapOptionsGui extends BlazeGui implements MapHost { private static final TranslatableComponent MAP_TYPES = Helpers.translate("blazemap.gui.minimap_options.map_types"); private static final TranslatableComponent LAYERS = Helpers.translate("blazemap.gui.minimap_options.layers"); private static final int WIDTH = 128, HEIGHT = 154; @@ -29,7 +32,7 @@ public static void open() { Minecraft.getInstance().setScreen(new MinimapOptionsGui()); } - private final MapRenderer mapRenderer = new MapRenderer(0, 0, Helpers.identifier("dynamic/map/minimap_preview"), MinimapRenderer.MIN_ZOOM, MinimapRenderer.MAX_ZOOM, false); + private final MapRenderer mapRenderer = new MapRenderer(0, 0, BlazeMap.resource("dynamic/map/minimap_preview"), MinimapRenderer.MIN_ZOOM, MinimapRenderer.MAX_ZOOM); private final MinimapConfigSynchronizer synchronizer = MinimapRenderer.INSTANCE.synchronizer; private final WidgetConfigFacade configFacade = new WidgetConfigFacade(BlazeMapConfig.CLIENT.minimap, mapRenderer); private final MinimapWidget minimap = new MinimapWidget(mapRenderer, configFacade, true); @@ -49,25 +52,30 @@ public void toggleLayer(Key layerID) { } @Override - public MapType getMapType() { - return mapRenderer.getMapType(); + public boolean isOverlayVisible(Key overlayID) { + return mapRenderer.isOverlayVisible(overlayID); } @Override - public void setMapType(MapType map) { - mapRenderer.setMapType(map); + public void toggleOverlay(Key overlayID) { + synchronizer.toggleOverlay(overlayID); } @Override - public void drawTooltip(PoseStack stack, Component component, int x, int y) { - renderTooltip(stack, component, x, y); + public MapType getMapType() { + return mapRenderer.getMapType(); } @Override - public Iterable getChildren() { - return children(); + public void setMapType(MapType map) { + mapRenderer.setMapType(map); } + /*@Override + public void drawTooltip(PoseStack stack, int x, int y, List lines) { + renderTooltip(stack, lines.stream().map(Component::getVisualOrderText).toList(), x, y); + }*/ + @Override protected void init() { super.init(); diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapRenderer.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapRenderer.java index ed7ef481..6891e6ad 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapRenderer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapRenderer.java @@ -6,9 +6,10 @@ import net.minecraft.core.BlockPos; import net.minecraftforge.client.gui.ForgeIngameGui; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.config.BlazeMapConfig; -import com.eerussianguy.blazemap.api.maps.IScreenSkipsMinimap; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.engine.render.MapRenderer; +import com.eerussianguy.blazemap.lib.Helpers; import com.eerussianguy.blazemap.profiling.Profilers; import com.mojang.blaze3d.vertex.PoseStack; @@ -22,7 +23,7 @@ public class MinimapRenderer implements AutoCloseable { private final MinimapWidget minimap; public MinimapRenderer() { - this.mapRenderer = new MapRenderer(0, 0, Helpers.identifier("dynamic/map/minimap"), MIN_ZOOM, MAX_ZOOM, true) + this.mapRenderer = new MapRenderer(0, 0, BlazeMap.resource("dynamic/map/minimap"), MIN_ZOOM, MAX_ZOOM) .setProfilers(Profilers.Minimap.DRAW_TIME_PROFILER, Profilers.Minimap.TEXTURE_TIME_PROFILER); this.synchronizer = new MinimapConfigSynchronizer(mapRenderer, BlazeMapConfig.CLIENT.minimap); this.minimap = new MinimapWidget(mapRenderer, BlazeMapConfig.CLIENT.minimap, false); @@ -30,7 +31,7 @@ public MinimapRenderer() { public void draw(PoseStack stack, MultiBufferSource buffers, ForgeIngameGui gui, int width, int height) { Minecraft mc = Minecraft.getInstance(); - if(mc.screen instanceof IScreenSkipsMinimap) return; + if(mc.screen != null) return; LocalPlayer player = Helpers.getPlayer(); if(player == null) return; diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapWidget.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapWidget.java index b92b6541..49a3215f 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapWidget.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/MinimapWidget.java @@ -9,10 +9,11 @@ import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.config.BlazeMapConfig; import com.eerussianguy.blazemap.config.MinimapConfigFacade.IWidgetConfig; -import com.eerussianguy.blazemap.gui.MouseSubpixelSmoother; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; -import com.eerussianguy.blazemap.util.RenderHelper; +import com.eerussianguy.blazemap.engine.render.MapRenderer; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.util.MouseSubpixelSmoother; import com.electronwill.nightconfig.core.io.WritingException; import com.mojang.blaze3d.platform.Window; import com.mojang.blaze3d.vertex.PoseStack; @@ -55,7 +56,9 @@ public void render(PoseStack stack, MultiBufferSource buffers) { RenderHelper.fillRect(stack.last().pose(), width + BORDER_SIZE*2, height + BORDER_SIZE*2, Colors.WIDGET_BACKGROUND); stack.popPose(); - map.render(stack, buffers); + RenderHelper.renderWithScissorNative(posX, posY, width, height, () -> { + map.render(stack, buffers); + }); if(editor){ stack.pushPose(); diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapGui.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapGui.java index 2a6855ef..f133f484 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapGui.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapGui.java @@ -1,53 +1,53 @@ package com.eerussianguy.blazemap.feature.maps; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import java.util.stream.Collectors; import org.lwjgl.glfw.GLFW; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.EditBox; -import net.minecraft.client.gui.components.Widget; -import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.TextComponent; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; -import com.eerussianguy.blazemap.api.maps.TileResolution; -import com.eerussianguy.blazemap.config.BlazeMapConfig; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapAPI; import com.eerussianguy.blazemap.api.BlazeRegistry; -import com.eerussianguy.blazemap.api.maps.IScreenSkipsMinimap; -import com.eerussianguy.blazemap.api.maps.Layer; import com.eerussianguy.blazemap.api.maps.MapType; -import com.eerussianguy.blazemap.engine.BlazeMapAsync; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.api.maps.TileResolution; +import com.eerussianguy.blazemap.config.BlazeMapConfig; import com.eerussianguy.blazemap.feature.BlazeMapFeaturesClient; -import com.eerussianguy.blazemap.feature.atlas.AtlasExporter; -import com.eerussianguy.blazemap.feature.atlas.AtlasTask; -import com.eerussianguy.blazemap.gui.Image; -import com.eerussianguy.blazemap.gui.MouseSubpixelSmoother; -import com.eerussianguy.blazemap.profiling.overlay.ProfilingRenderer; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.feature.atlas.*; +import com.eerussianguy.blazemap.feature.maps.ui.*; +import com.eerussianguy.blazemap.feature.maps.ui.NamedMapComponentButton.*; +import com.eerussianguy.blazemap.lib.ObjHolder; +import com.eerussianguy.blazemap.lib.gui.components.Image; +import com.eerussianguy.blazemap.lib.gui.components.LineContainer; +import com.eerussianguy.blazemap.lib.gui.components.Placeholder; +import com.eerussianguy.blazemap.lib.gui.components.VanillaComponents; +import com.eerussianguy.blazemap.lib.gui.core.*; +import com.eerussianguy.blazemap.lib.gui.fragment.*; +import com.eerussianguy.blazemap.lib.gui.util.VisibilityController; import com.eerussianguy.blazemap.profiling.Profiler; -import com.eerussianguy.blazemap.util.RenderHelper; import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.Tesselator; -public class WorldMapGui extends Screen implements IScreenSkipsMinimap, IMapHost { - private static final TextComponent EMPTY = new TextComponent(""); - private static final ResourceLocation ICON = Helpers.identifier("textures/mod_icon.png"); - private static final ResourceLocation NAME = Helpers.identifier("textures/mod_name.png"); +public class WorldMapGui extends Screen implements FragmentHost, TooltipService { + private static final ResourceLocation HEADER_MAPS = BlazeMap.resource("textures/map_icons/header_maps.png"); + private static final ResourceLocation HEADER_LAYERS = BlazeMap.resource("textures/map_icons/header_layers.png"); + private static final ResourceLocation HEADER_OVERLAYS = BlazeMap.resource("textures/map_icons/header_overlays.png"); + private static final ResourceLocation BLAZEMAP_ICON = BlazeMap.resource("textures/mod_icon.png"); + private static final ResourceLocation BLAZEMAP_NAME = BlazeMap.resource("textures/mod_name.png"); public static final double MIN_ZOOM = 0.125, MAX_ZOOM = 8; + private static final int MARGIN = 5; private static final Profiler.TimeProfiler renderTime = new Profiler.TimeProfilerSync("world_map_render", 10); private static final Profiler.TimeProfiler uploadTime = new Profiler.TimeProfilerSync("world_map_upload", 10); - private static boolean showWidgets = true, renderDebug = false; + private static final VisibilityController visibilityController = new VisibilityController(); + private static boolean renderDebug = false; public static void open() { Minecraft.getInstance().setScreen(new WorldMapGui()); @@ -59,347 +59,146 @@ public static void apply(Consumer function) { } } + private static final WorldMapHotkey[] HOTKEYS = new WorldMapHotkey[] { + new WorldMapHotkey("LMB", "Drag to pan the map"), + new WorldMapHotkey("RMB", "Open context menu"), + new WorldMapHotkey("Scroll", "Zoom in / out"), + new WorldMapHotkey("F1", "Toggle map UI"), + new WorldMapHotkey("F3", "Toggle debug info"), + new WorldMapHotkey("F12", "Export atlas"), + new WorldMapHotkey("W A S D", "Pan the map") + }; + // ================================================================================================================= - private double zoom = 1; - private final ResourceKey dimension; - private final MapRenderer mapRenderer; + private final ResourceKey dimension = Minecraft.getInstance().level.dimension(); + private final List mapTypes = BlazeMapAPI.MAPTYPES.keys().stream().map(BlazeRegistry.Key::value).filter(m -> m.shouldRenderInDimension(dimension)).toList(); + private final List overlays = BlazeMapFeaturesClient.OVERLAYS.stream().map(BlazeRegistry.Key::value).filter(o -> o.shouldRenderInDimension(dimension)).toList(); private final MapConfigSynchronizer synchronizer; - private final List mapTypes; - private final int layersBegin; - private final MouseSubpixelSmoother mouse; - private Widget legend; - private EditBox search; - private final Coordination coordination = new Coordination(); - private double rawMouseX = -1, rawMouseY = -1; - private WorldMapPopup contextMenu; + private final InteractiveMapDisplay map; + private AbsoluteContainer windows, components; + private VolatileContainer volatiles; + private MetaContainer root; + private Placeholder legend; public WorldMapGui() { - super(EMPTY); - mapRenderer = new MapRenderer(-1, -1, Helpers.identifier("dynamic/map/worldmap"), MIN_ZOOM, MAX_ZOOM, true).setProfilers(renderTime, uploadTime); - synchronizer = new MapConfigSynchronizer(mapRenderer, BlazeMapConfig.CLIENT.worldMap); - dimension = Minecraft.getInstance().level.dimension(); - mapTypes = BlazeMapAPI.MAPTYPES.keys().stream().map(BlazeRegistry.Key::value).filter(m -> m.shouldRenderInDimension(dimension)).collect(Collectors.toUnmodifiableList()); - layersBegin = 50 + (mapTypes.size() * 20); - mouse = new MouseSubpixelSmoother(); - zoom = mapRenderer.getZoom(); - - mapRenderer.setSearchHost(active -> { - if(search != null) { - search.visible = active; - } - }); - } - - @Override - public boolean isLayerVisible(BlazeRegistry.Key layerID) { - return mapRenderer.isLayerVisible(layerID); - } - - @Override - public void toggleLayer(BlazeRegistry.Key layerID) { - synchronizer.toggleLayer(layerID); - } + super(TextComponent.EMPTY); - @Override - public MapType getMapType() { - return mapRenderer.getMapType(); + map = new InteractiveMapDisplay(BlazeMap.resource("dynamic/map/worldmap"), MIN_ZOOM, MAX_ZOOM); + var renderer = map.getRenderer(); + synchronizer = new MapConfigSynchronizer(renderer, BlazeMapConfig.CLIENT.worldMap); + map.setSynchronizer(synchronizer).setProfilers(renderTime, uploadTime).onMapChange(this::updateLegend); } @Override - public void setMapType(MapType map) { - synchronizer.setMapType(map); - search.setValue(""); - updateLegend(); - } - - @Override - public void drawTooltip(PoseStack stack, Component component, int x, int y) { - renderTooltip(stack, component, x, y); - } - - @Override - public Iterable getChildren() { - return children(); + public void drawTooltip(PoseStack stack, int x, int y, List lines) { + renderTooltip(stack, lines.stream().map(Component::getVisualOrderText).collect(Collectors.toList()), x, y); } @Override protected void init() { - double scale = getMinecraft().getWindow().getGuiScale(); - mapRenderer.resize((int) (Math.ceil(width * scale / MAX_ZOOM) * MAX_ZOOM), (int) (Math.ceil(height * scale / MAX_ZOOM) * MAX_ZOOM)); - - addRenderableOnly(new Image(ICON, 5, 5, 20, 20)); - addRenderableOnly(new Image(NAME, 30, 5, 110, 20)); - int y = 20; - for(MapType mapType : mapTypes) { - BlazeRegistry.Key key = mapType.getID(); - int px = 7, py = (y += 20); - addRenderableWidget(new MapTypeButton(px, py, 16, 16, key, this)); - MapType map = key.value(); - int layerY = layersBegin; - List> childLayers = map.getLayers().stream().collect(Collectors.toList()); - Collections.reverse(childLayers); - for(BlazeRegistry.Key layer : childLayers) { - if(layer.value().isOpaque()) continue; - LayerButton lb = new LayerButton(px, layerY, 16, 16, layer, map, this); - layerY += 20; - lb.checkVisible(); - addRenderableWidget(lb); + // UI LAYERS + volatiles = new VolatileContainer(0); + windows = new AbsoluteContainer(0); + components = new AbsoluteContainer(MARGIN); + AbsoluteContainer background = new AbsoluteContainer(0); + background.add(map.setVolatiles(volatiles).setSize(width, height), 0, 0); + root = addRenderableWidget(new MetaContainer(width, height).add(background, components, windows, volatiles).setInputConsumer(map)); + visibilityController.clear().add(components, windows, volatiles); + + // BRANDING + var brand_icon = new Image(BLAZEMAP_ICON, 20, 20); + var brand_logo = new Image(BLAZEMAP_NAME, 110, 20); + components.add(brand_icon, ContainerAnchor.TOP_LEFT); + components.add(brand_logo, ContainerAnchor.TOP_CENTER); + + // MAPS AND LAYERS + LineContainer maps = new LineContainer(ContainerAxis.HORIZONTAL, ContainerDirection.POSITIVE, 2).withBackground(); + components.anchor(maps,brand_icon, ContainerAxis.HORIZONTAL, ContainerDirection.POSITIVE); + maps.add(new Image(HEADER_MAPS, 16, 16).tooltip(new TextComponent("Maps"))); + maps.addSpacer(); + List layerSets = new ArrayList<>(); + for(var mapType : mapTypes) { + LineContainer layerSet = new LineContainer(ContainerAxis.VERTICAL, ContainerDirection.NEGATIVE, 2).withBackground(); + components.anchor(layerSet, brand_icon, ContainerAxis.VERTICAL, ContainerDirection.POSITIVE); + layerSets.add(layerSet); + maps.add(new MapTypeButton(mapType.getID(), map, layerSets, layerSet)); + + layerSet.setVisible(map.getMapType().getID().equals(mapType.getID())); + for(var layer : mapType.getLayers()) { + layerSet.add(new LayerButton(layer, map)); } - } - - search = addRenderableWidget(new EditBox(getMinecraft().font, (width - 120) / 2, height - 15, 120, 12, EMPTY)); - search.setResponder(mapRenderer::setSearch); - mapRenderer.pingSearchHost(); - + layerSet.addSpacer().add(new Image(HEADER_LAYERS, 16, 16).tooltip(new TextComponent("Layers"))); + } + + // OVERLAYS + LineContainer overlaySet = new LineContainer(ContainerAxis.HORIZONTAL, ContainerDirection.POSITIVE, 2).withBackground(); + overlaySet.add(new Image(HEADER_OVERLAYS, 16, 16).tooltip(new TextComponent("Overlays"))); + overlaySet.addSpacer(); + for(var overlay : overlays) { + overlaySet.add(new OverlayButton(overlay.getID(), map)); + } + components.add(overlaySet, ContainerAnchor.BOTTOM_LEFT); + + // SEARCH + ObjHolder text = new ObjHolder<>(); + BaseComponent search = VanillaComponents.makeTextField(getMinecraft().font, 120, 15, text); + components.add(search, ContainerAnchor.BOTTOM_CENTER); + text.setResponder(map.getRenderer()::setSearch); + var renderer = map.getRenderer(); + renderer.setSearchHost(search::setVisible); + renderer.pingSearchHost(); + + // HOTKEYS + var hotkeys = new LineContainer(ContainerAxis.VERTICAL, ContainerDirection.POSITIVE, 3).withBackground().with(HOTKEYS); + components.anchor(hotkeys, overlaySet, ContainerAxis.VERTICAL, ContainerDirection.NEGATIVE); + + // LEGEND + legend = new Placeholder(); + components.add(legend, ContainerAnchor.BOTTOM_RIGHT); updateLegend(); - } - - private void updateLegend() { - legend = mapRenderer.getMapType().getLayers().iterator().next().value().getLegendWidget(); - } - - @Override - public boolean mouseDragged(double mouseX, double mouseY, int button, double draggedX, double draggedY) { - setMouse(mouseX, mouseY); - if(button == GLFW.GLFW_MOUSE_BUTTON_1) { - double scale = getMinecraft().getWindow().getGuiScale(); - mouse.addMovement(draggedX * scale / zoom, draggedY * scale / zoom); - mapRenderer.moveCenter(-mouse.movementX(), -mouse.movementY()); - return true; - } - return super.mouseDragged(mouseX, mouseY, button, draggedX, draggedY); - } - @Override - public void mouseMoved(double mouseX, double mouseY) { - setMouse(mouseX, mouseY); - super.mouseMoved(mouseX, mouseY); - } + // SCALE + MapScaleDisplay scale = new MapScaleDisplay(256, map.getRenderer()); + components.anchor(scale, legend, ContainerAxis.HORIZONTAL, ContainerDirection.NEGATIVE); - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double scroll) { - boolean zoomed; - if(scroll > 0) { - zoomed = synchronizer.zoomIn(); - } - else { - zoomed = synchronizer.zoomOut(); - } - zoom = mapRenderer.getZoom(); - setMouse(mouseX, mouseY); - return zoomed; + components.add(new AtlasExportProgress(), ContainerAnchor.TOP_RIGHT); + components.anchor(new WorldMapDebug(map.getRenderer().debug, map.getCoordination(), renderTime, uploadTime, () -> renderDebug), maps, ContainerAxis.VERTICAL, ContainerDirection.POSITIVE); } - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - setMouse(mouseX, mouseY); - - if(contextMenu != null){ // On existing menu, pass into - var result = contextMenu.onClick((int) rawMouseX, (int) rawMouseY, button); - if(result.shouldDismiss) { - contextMenu = null; - } - if(result.wasHandled) { - return true; - } - } - - if(super.mouseClicked(mouseX, mouseY, button)) { // If super handled, exit - return true; - } - - if(button == GLFW.GLFW_MOUSE_BUTTON_2) { // If right click open new menu - int scale = (int) getMinecraft().getWindow().getGuiScale(); - contextMenu = new WorldMapPopup(coordination, width * scale, height * scale, mapRenderer.getVisibleLayers()); - return true; - } - - return false; - } + private void updateLegend() { + var legend = WrappedComponent.ofNullable(map.getMapType().getLayers().iterator().next().value().getLegendWidget()); - private void setMouse(double mouseX, double mouseY) { - double scale = getMinecraft().getWindow().getGuiScale(); - this.rawMouseX = mouseX * scale; - this.rawMouseY = mouseY * scale; - coordination.calculate((int) this.rawMouseX, (int) this.rawMouseY, mapRenderer.getBeginX(), mapRenderer.getBeginZ(), mapRenderer.getZoom()); - if(contextMenu != null) { - contextMenu.setMouse(coordination.mousePixelX, coordination.mousePixelY); + if(legend == null) { + this.legend.clear(); + } else { + this.legend.add(legend); } } @Override - public void render(PoseStack stack, int i0, int i1, float f0) { - float scale = (float) getMinecraft().getWindow().getGuiScale(); - - stack.pushPose(); - stack.scale(1F / scale, 1F / scale, 1); - var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); - mapRenderer.render(stack, buffers); - buffers.endBatch(); - if(contextMenu != null){ - contextMenu.render(stack, i0, i1, f0); - } - stack.popPose(); - - if(legend != null) { - stack.pushPose(); - stack.translate(width - 5, height - 5, 0); - legend.render(stack, -1, -1, 0); - stack.popPose(); - } - - if(showWidgets) { - renderAtlasExportProgress(stack, scale); - - int maps = mapTypes.size(); - if(maps > 0) { - stack.pushPose(); - stack.translate(5, 38, 0); - RenderHelper.fillRect(stack.last().pose(), 20, maps * 20, Colors.WIDGET_BACKGROUND); - stack.popPose(); - } - long layers = mapRenderer.getMapType().getLayers().stream().map(k -> k.value()).filter(l -> !l.isOpaque() && l.shouldRenderInDimension(dimension)).count(); - if(layers > 0) { - stack.pushPose(); - stack.translate(5, layersBegin - 2, 0); - RenderHelper.fillRect(stack.last().pose(), 20, layers * 20, Colors.WIDGET_BACKGROUND); - stack.popPose(); - } - stack.pushPose(); - super.render(stack, i0, i1, f0); - stack.popPose(); - } - - if(renderDebug) { - stack.pushPose(); - renderDebug(stack); - stack.popPose(); - - stack.pushPose(); - stack.scale(1F / scale, 1F / scale, 1); - renderCoordination(stack, scale); - stack.popPose(); - } - } - - private void renderAtlasExportProgress(PoseStack stack, float scale) { - AtlasTask task = AtlasExporter.getTask(); - if(task == null) return; - Font font = Minecraft.getInstance().font; - stack.pushPose(); - - stack.translate(width - 205, 5, 0); // Go to corner - RenderHelper.fillRect(stack.last().pose(), 200, 30, Colors.WIDGET_BACKGROUND); // draw background - - // Process flashing "animation" - int textColor = Colors.WHITE; - long flashUntil = ((long)task.getFlashUntil()) * 1000L; - long now = System.currentTimeMillis(); - if(task.isErrored() || (flashUntil >= now && now % 333 < 166)) { - textColor = 0xFFFF0000; - } - - // Render progress text - int total = task.getTilesTotal(); - int current = task.getTilesCurrent(); - font.draw(stack, String.format("Exporting 1:%d", task.resolution.pixelWidth), 5, 5, textColor); - String operation = switch(task.getStage()){ - case QUEUED -> "queued"; - case CALCULATING -> "calculating"; - case STITCHING -> String.format("stitching %d / %d tiles", current, total); - case SAVING -> "saving"; - case COMPLETE -> "complete"; - }; - font.draw(stack, operation, 195 - font.width(operation), 5, textColor); - - // Render progress bar - double progress = ((double)current) / ((double)total); - stack.translate(5, 17, 0); - RenderHelper.fillRect(stack.last().pose(), 190, 10, Colors.LABEL_COLOR); - RenderHelper.fillRect(stack.last().pose(), (int)(190*progress), 10, textColor); - - stack.popPose(); - } - - private void renderCoordination(PoseStack stack, float scale){ - if(rawMouseX == -1 || rawMouseY == -1) return; - - stack.pushPose(); - stack.translate(coordination.regionPixelX, coordination.regionPixelY, 0.1); - RenderHelper.fillRect(stack.last().pose(), coordination.regionPixels, coordination.regionPixels, 0x400000FF); - stack.popPose(); - - stack.pushPose(); - stack.translate(coordination.chunkPixelX, coordination.chunkPixelY, 0.2); - RenderHelper.fillRect(stack.last().pose(), coordination.chunkPixels, coordination.chunkPixels, 0x6000FF00); - stack.popPose(); - - stack.pushPose(); - stack.translate(coordination.blockPixelX, coordination.blockPixelY, 0.3); - RenderHelper.fillRect(stack.last().pose(), coordination.blockPixels, coordination.blockPixels, 0x80FF0000); - stack.popPose(); - - stack.pushPose(); - stack.translate(width * scale / 2, 10, 1); - stack.scale(3, 3, 0); - Font font = getMinecraft().font; - String region = String.format("Rg %d %d | px: %d %d", coordination.regionX, coordination.regionZ, coordination.regionPixelX, coordination.regionPixelY); - font.draw(stack, region, 0, 0, 0x0000FF); - String chunk = String.format("Ch %d %d | px: %d %d", coordination.chunkX, coordination.chunkZ, coordination.chunkPixelX, coordination.chunkPixelY); - font.draw(stack, chunk, 0, 10, 0x00FF00); - String block = String.format("Bl %d %d | px: %d %d", coordination.blockX, coordination.blockZ, coordination.blockPixelX, coordination.blockPixelY); - font.draw(stack, block, 0, 20, 0xFF0000); - stack.popPose(); - } - - private void renderDebug(PoseStack stack) { - stack.translate(32, 25, 0); - RenderHelper.fillRect(stack.last().pose(), 135, 110, 0x80000000); - font.draw(stack, "Debug Info", 5, 5, 0xFFFF0000); - stack.translate(5, 20, 0); - stack.scale(0.5F, 0.5F, 1); - - font.draw(stack, "Atlas Time Profiling:", 0, 0, -1); - var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); - ProfilingRenderer.drawTimeProfiler(renderTime, 12, "Render", font, stack.last().pose(), buffers); - ProfilingRenderer.drawTimeProfiler(uploadTime, 24, "Upload", font, stack.last().pose(), buffers); - buffers.endBatch(); - - MapRenderer.DebugInfo debug = mapRenderer.debug; - int y = 30; - font.draw(stack, String.format("Renderer Size: %d x %d", debug.rw, debug.rh), 0, y += 12, -1); - font.draw(stack, String.format("Renderer Zoom: %sx", debug.zoom), 0, y += 12, -1); - font.draw(stack, String.format("Atlas Size: %d x %d", debug.mw, debug.mh), 0, y += 12, -1); - font.draw(stack, String.format("Atlas Frustum: [%d , %d] to [%d , %d]", debug.bx, debug.bz, debug.ex, debug.ez), 0, y += 12, -1); - - font.draw(stack, String.format("Region Matrix: %d x %d", debug.ox, debug.oz), 0, y += 18, -1); - font.draw(stack, String.format("Active Layers: %d", debug.layers), 0, y += 12, -1); - font.draw(stack, String.format("Stitching: %s", debug.stitching), 0, y += 12, 0xFF0088FF); - font.draw(stack, String.format("Parallel Pool: %d", BlazeMapAsync.instance().cruncher.poolSize()), 0, y += 12, 0xFFFFFF00); - - font.draw(stack, String.format("Addon Labels: %d", debug.labels), 0, y += 18, -1); - font.draw(stack, String.format("Player Waypoints: %d", debug.waypoints), 0, y += 12, -1); + public boolean consumeFragment(BaseFragment fragment) { + HostWindowComponent window = new HostWindowComponent(fragment, volatiles).setCloser(windows::remove); + windows.add(window, ContainerAnchor.MIDDLE_CENTER); + return true; } @Override - public void onClose() { - mapRenderer.close(); - synchronizer.save(); - super.onClose(); + public boolean mouseDragged(double p_94699_, double p_94700_, int p_94701_, double p_94702_, double p_94703_) { + return super.mouseDragged(p_94699_, p_94700_, p_94701_, p_94702_, p_94703_); } @Override - public boolean keyPressed(int key, int x, int y) { + public boolean keyPressed(int key, int scancode, int modifiers) { if(key == GLFW.GLFW_KEY_F1) { - showWidgets = !showWidgets; + visibilityController.toggleVisible(); return true; } if(key == GLFW.GLFW_KEY_F12) { - AtlasExporter.exportAsync(new AtlasTask(this.dimension, this.getMapType().getID(), this.mapRenderer.getVisibleLayers(), TileResolution.FULL, this.mapRenderer.getCenterRegion())); + AtlasExporter.exportAsync(new AtlasTask(this.dimension, map.getMapType().getID(), map.getRenderer().getVisibleLayers(), TileResolution.FULL, map.getRenderer().getCenterRegion())); return true; } @@ -408,37 +207,23 @@ public boolean keyPressed(int key, int x, int y) { return true; } - if(!search.isFocused()) { - if(key == BlazeMapFeaturesClient.KEY_MAPS.getKey().getValue()) { - this.onClose(); - return true; - } + if(root.keyPressed(key, scancode, modifiers)) return true; + if(super.keyPressed(key, scancode, modifiers)) return true; - int dx = 0; - int dz = 0; - if(key == GLFW.GLFW_KEY_W) { - dz -= 16; - } - if(key == GLFW.GLFW_KEY_S) { - dz += 16; - } - if(key == GLFW.GLFW_KEY_D) { - dx += 16; - } - if(key == GLFW.GLFW_KEY_A) { - dx -= 16; - } - if(dx != 0 || dz != 0) { - mapRenderer.moveCenter(dx, dz); - return true; - } + if(key == BlazeMapFeaturesClient.KEY_MAPS.getKey().getValue()) { + this.onClose(); + return true; } - return super.keyPressed(key, x, y); + + return false; } @Override - public Minecraft getMinecraft() { - return Minecraft.getInstance(); + public void onClose() { + map.getRenderer().close(); + synchronizer.save(); + visibilityController.clear(); + super.onClose(); } public void addInspector(MDInspectorWidget widget) { diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapMenu.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapMenu.java index 3d0a6885..b96aa6e7 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapMenu.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapMenu.java @@ -4,65 +4,61 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.TextComponent; import net.minecraft.network.chat.TranslatableComponent; -import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; - import net.minecraftforge.fml.loading.FMLEnvironment; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeMapReferences; -import com.eerussianguy.blazemap.api.builtin.BlockColorMD; -import com.eerussianguy.blazemap.api.event.DimensionChangedEvent; import com.eerussianguy.blazemap.api.event.MapMenuSetupEvent; -import com.eerussianguy.blazemap.api.event.MapMenuSetupEvent.*; -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; -import com.eerussianguy.blazemap.api.markers.Waypoint; -import com.eerussianguy.blazemap.api.pipeline.MasterDatum; +import com.eerussianguy.blazemap.api.event.MapMenuSetupEvent.MenuAction; +import com.eerussianguy.blazemap.api.event.MapMenuSetupEvent.MenuFolder; +import com.eerussianguy.blazemap.api.event.MapMenuSetupEvent.MenuItem; import com.eerussianguy.blazemap.engine.cache.ChunkMDCache; -import com.eerussianguy.blazemap.engine.client.BlazeMapClientEngine; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; +import com.eerussianguy.blazemap.engine.client.ClientEngine; +import com.eerussianguy.blazemap.feature.waypoints.WaypointEditorFragment; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointServiceClient; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; public class WorldMapMenu { private static final String BASE_PATH = "worldmap.menu."; private static final String BASE_LANG = "blazemap.gui.worldmap.menu."; private static final ResourceLocation BLAZE_POWDER = new ResourceLocation("minecraft", "textures/item/blaze_powder.png"); - private static final ResourceLocation MENU_NOOP = Helpers.identifier("map.menu.noop"); + private static final ResourceLocation MENU_NOOP = BlazeMap.resource("map.menu.noop"); private static final TranslatableComponent NOOP_TEXT = Helpers.translate("blazemap.gui.worldmap.menu.no_options"); public static final MapMenuSetupEvent.MenuAction NOOP = new MapMenuSetupEvent.MenuAction(MENU_NOOP, null, NOOP_TEXT, null); - private static IMarkerStorage waypointStore; - private static ResourceKey dimension; - public static MenuFolder waypoints(int blockX, int blockZ) { + WaypointServiceClient waypoints = WaypointServiceClient.instance(); + BlockPos local = new BlockPos(blockX, 0, blockZ); MenuFolder folder = makeFolder("waypoint", BlazeMapReferences.Icons.WAYPOINT, Colors.WHITE, makeAction("waypoint.new", BlazeMapReferences.Icons.WAYPOINT, - () -> waypointStore.add(new Waypoint(Helpers.identifier("new-waypoint-"+System.nanoTime()), dimension, new BlockPos(blockX, 0, blockZ), "New Waypoint")) + () -> new WaypointEditorFragment(local).open() ) ); - waypointStore.getAll().stream() - .filter(waypoint -> waypoint.getPosition().atY(0).distSqr(local) < 48) - .map(waypoint -> makeFolder("waypoint.options", waypoint.getIcon(), waypoint.getColor(), waypoint.getName(), - makeAction("waypoint.edit", null, null), + waypoints.iterate(waypoint -> { + if(waypoint.getPosition().atY(0).distSqr(local) < 40) return; + folder.add(makeFolder("waypoint.options", waypoint.getIcon(), waypoint.getColor(), waypoint.getName(), + makeAction("waypoint.edit", null, () -> new WaypointEditorFragment(waypoint).open()), makeAction("waypoint.hide", null, null), - makeAction("waypoint.delete", null, () -> waypointStore.remove(waypoint)) + makeAction("waypoint.delete", null, null) ) - ) - .forEach(folder::add); + ); + }); return folder; } public static MenuFolder debug(int blockX, int blockZ, int chunkX, int chunkZ, int regionX, int regionZ) { final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); MenuFolder folder = makeFolder("debug", BLAZE_POWDER, -1, - makeAction("debug.redraw_chunk_md", null, () -> BlazeMapClientEngine.forceRedrawFromMD(chunkPos)) + makeAction("debug.redraw_chunk_md", null, () -> ClientEngine.forceRedrawFromMD(chunkPos)) ); if(!FMLEnvironment.production) { - ChunkMDCache mdCache = BlazeMapClientEngine.getMDCache(chunkPos); + ChunkMDCache mdCache = ClientEngine.getMDCache(chunkPos); MenuFolder mdInspector = makeFolder("debug.inspect_chunk_md", null, -1); if(mdCache != null) { mdCache.data().forEach(md -> { @@ -80,24 +76,19 @@ public static MenuFolder debug(int blockX, int blockZ, int chunkX, int chunkZ, i return folder; } - public static void trackWaypointStore(DimensionChangedEvent evt) { - waypointStore = evt.waypoints; - dimension = evt.dimension; - } - private static MenuAction makeAction(String id, ResourceLocation icon, Runnable function) { - return new MenuAction(Helpers.identifier(BASE_PATH + id), icon, Helpers.translate(BASE_LANG + id), function); + return new MenuAction(BlazeMap.resource(BASE_PATH + id), icon, Helpers.translate(BASE_LANG + id), function); } private static MenuAction makeAction(String id, ResourceLocation icon, Component name, Runnable function) { - return new MenuAction(Helpers.identifier(BASE_PATH + id), icon, name, function); + return new MenuAction(BlazeMap.resource(BASE_PATH + id), icon, name, function); } private static MenuFolder makeFolder(String id, ResourceLocation icon, int tint, String name, MenuItem ... children){ - return new MenuFolder(Helpers.identifier(BASE_PATH + id), icon, tint, new TextComponent(name), children); + return new MenuFolder(BlazeMap.resource(BASE_PATH + id), icon, tint, new TextComponent(name), children); } private static MenuFolder makeFolder(String id, ResourceLocation icon, int tint, MenuItem ... children){ - return new MenuFolder(Helpers.identifier(BASE_PATH + id), icon, tint, Helpers.translate(BASE_LANG + id), children); + return new MenuFolder(BlazeMap.resource(BASE_PATH + id), icon, tint, Helpers.translate(BASE_LANG + id), children); } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapPopup.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapPopup.java index cdc4f1cb..92b9663d 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapPopup.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/WorldMapPopup.java @@ -9,22 +9,22 @@ import net.minecraft.client.gui.components.Widget; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.TextComponent; -import net.minecraft.network.chat.TranslatableComponent; import net.minecraft.resources.ResourceLocation; - import net.minecraftforge.common.MinecraftForge; +import com.eerussianguy.blazemap.BlazeMap; import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.event.MapMenuSetupEvent; import com.eerussianguy.blazemap.api.maps.Layer; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; -import com.eerussianguy.blazemap.util.IntHolder; -import com.eerussianguy.blazemap.util.RenderHelper; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.IntHolder; +import com.eerussianguy.blazemap.lib.RenderHelper; import com.mojang.blaze3d.vertex.PoseStack; +@Deprecated public class WorldMapPopup implements Widget { - private static final ResourceLocation MENU_ROOT = Helpers.identifier("map.menu"); + private static final ResourceLocation MENU_ROOT = BlazeMap.resource("map.menu"); private static final TextComponent MENU_ROOT_TEXT = new TextComponent(""); private static final int MIN_WIDTH = 160; @@ -36,10 +36,15 @@ public class WorldMapPopup implements Widget { private WorldMapPopup activeChild = null; private PopupItem lastClicked, hovered; - public WorldMapPopup(Coordination coordination, int width, int height, List> layers) { + public WorldMapPopup(Coordination coordination, int width, int height, List> layers, List> overlays) { // Kick off menu creation MapMenuSetupEvent.MenuFolder container = new MapMenuSetupEvent.MenuFolder(MENU_ROOT, null, MENU_ROOT_TEXT); - MinecraftForge.EVENT_BUS.post(new MapMenuSetupEvent(container, layers, coordination.blockX, coordination.blockZ, coordination.chunkX, coordination.chunkZ, coordination.regionX, coordination.regionZ)); + MinecraftForge.EVENT_BUS.post(new MapMenuSetupEvent( + container, layers, overlays, Minecraft.getInstance().level.dimension(), + coordination.blockX, coordination.blockZ, + coordination.chunkX, coordination.chunkZ, + coordination.regionX, coordination.regionZ) + ); if(container.size() == 0) { container.add(WorldMapMenu.NOOP); } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/InteractiveMapDisplay.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/InteractiveMapDisplay.java new file mode 100644 index 00000000..ebb483f5 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/InteractiveMapDisplay.java @@ -0,0 +1,109 @@ +package com.eerussianguy.blazemap.feature.maps.ui; + +import org.lwjgl.glfw.GLFW; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.feature.maps.Coordination; +import com.eerussianguy.blazemap.feature.maps.MapConfigSynchronizer; +import com.eerussianguy.blazemap.lib.gui.core.UIEventListener; +import com.eerussianguy.blazemap.lib.gui.core.VolatileContainer; +import com.eerussianguy.blazemap.lib.gui.trait.KeyboardControls; +import com.eerussianguy.blazemap.lib.gui.util.MouseSubpixelSmoother; +import com.mojang.blaze3d.platform.Window; + +public class InteractiveMapDisplay extends MapDisplay implements UIEventListener, KeyboardControls { + private final Window window = Minecraft.getInstance().getWindow(); + private final MouseSubpixelSmoother mouse = new MouseSubpixelSmoother(); + private final Coordination coordination = new Coordination(); + private VolatileContainer volatiles; + private double zoom; + + public InteractiveMapDisplay(ResourceLocation mapLocation, double minZoom, double maxZoom) { + this(0, 0, mapLocation, minZoom, maxZoom); + } + + public InteractiveMapDisplay(int width, int height, ResourceLocation mapLocation, double minZoom, double maxZoom) { + super(width, height, mapLocation, minZoom, maxZoom); + zoom = renderer.getZoom(); + } + + @Override + public MapDisplay setSynchronizer(MapConfigSynchronizer synchronizer) { + zoom = renderer.getZoom(); // Needed to get the correct zoom level when initializing + return super.setSynchronizer(synchronizer); + } + + public InteractiveMapDisplay setVolatiles(VolatileContainer volatiles) { + this.volatiles = volatiles; + return this; + } + + public Coordination getCoordination() { + return coordination; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double draggedX, double draggedY) { + setMouse(mouseX, mouseY); + if(button == GLFW.GLFW_MOUSE_BUTTON_1) { + double scale = window.getGuiScale(); + mouse.addMovement(draggedX * scale / zoom, draggedY * scale / zoom); + renderer.moveCenter(-mouse.movementX(), -mouse.movementY()); + return true; + } + return false; + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + setMouse(mouseX, mouseY); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scroll) { + boolean zoomed; + if(scroll > 0) { + zoomed = synchronizer.zoomIn(); + } + else { + zoomed = synchronizer.zoomOut(); + } + zoom = renderer.getZoom(); + setMouse(mouseX, mouseY); + return zoomed; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + setMouse(mouseX, mouseY); + + if(button == GLFW.GLFW_MOUSE_BUTTON_2) { + // TODO: create popup into volatiles. + return true; + } + + return false; + } + + private void setMouse(double mouseX, double mouseY) { + double scale = window.getGuiScale(); + coordination.calculate((int)(mouseX * scale), (int)(mouseY * scale), renderer.getBeginX(), renderer.getBeginZ(), renderer.getZoom()); + } + + @Override + public boolean keyPressed(int key, int scancode, int modifiers) { + int dx = 0, dz = 0; + + if( isKeyUp (key) ){ dz -= 16; } + if( isKeyDown (key) ){ dz += 16; } + if( isKeyRight (key) ){ dx += 16; } + if( isKeyLeft (key) ){ dx -= 16; } + + if(dx != 0 || dz != 0) { + renderer.moveCenter((int)(dx / zoom), (int)(dz / zoom)); + return true; + } + return false; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapDisplay.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapDisplay.java new file mode 100644 index 00000000..276bb6de --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapDisplay.java @@ -0,0 +1,98 @@ +package com.eerussianguy.blazemap.feature.maps.ui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.engine.render.MapRenderer; +import com.eerussianguy.blazemap.feature.maps.MapConfigSynchronizer; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.eerussianguy.blazemap.profiling.Profiler; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; + +public class MapDisplay extends BaseComponent implements MapHost { + protected final double maxZoom; + protected final MapRenderer renderer; + protected MapConfigSynchronizer synchronizer; + private Runnable onMapChange = () -> {}; + + public MapDisplay(ResourceLocation mapLocation, double minZoom, double maxZoom) { + this(0, 0, mapLocation, minZoom, maxZoom); + } + + public MapDisplay(int width, int height, ResourceLocation mapLocation, double minZoom, double maxZoom) { + this.renderer = new MapRenderer(width, height, mapLocation, minZoom, maxZoom); + this.maxZoom = maxZoom; + } + + public MapDisplay setSynchronizer(MapConfigSynchronizer synchronizer) { + this.synchronizer = synchronizer; + return this; + } + + public MapDisplay setProfilers(Profiler.TimeProfiler render, Profiler.TimeProfiler upload) { + this.renderer.setProfilers(render, upload); + return this; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + float scale = (float) Minecraft.getInstance().getWindow().getGuiScale(); + stack.scale(1F / scale, 1F / scale, 1); + var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + renderer.render(stack, buffers); + buffers.endBatch(); + } + + public MapRenderer getRenderer() { + return renderer; + } + + public void onMapChange(Runnable function) { + this.onMapChange = function; + } + + @Override + public MapDisplay setSize(int w, int h) { + double scale = Minecraft.getInstance().getWindow().getGuiScale(); + renderer.resize((int) (Math.ceil(w * scale / maxZoom) * maxZoom), (int) (Math.ceil(h * scale / maxZoom) * maxZoom)); + return super.setSize(w, h); + } + + @Override + public boolean isLayerVisible(BlazeRegistry.Key layerID) { + return renderer.isLayerVisible(layerID); + } + + @Override + public void toggleLayer(BlazeRegistry.Key layerID) { + synchronizer.toggleLayer(layerID); + } + + @Override + public boolean isOverlayVisible(BlazeRegistry.Key overlayID) { + return renderer.isOverlayVisible(overlayID); + } + + @Override + public void toggleOverlay(BlazeRegistry.Key overlayID) { + synchronizer.toggleOverlay(overlayID); + } + + @Override + public MapType getMapType() { + return renderer.getMapType(); + } + + @Override + public void setMapType(MapType map) { + if(synchronizer.setMapType(map)) { + onMapChange.run(); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapHost.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapHost.java new file mode 100644 index 00000000..b8f3ac28 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapHost.java @@ -0,0 +1,17 @@ +package com.eerussianguy.blazemap.feature.maps.ui; + +import com.eerussianguy.blazemap.api.BlazeRegistry.Key; +import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.Overlay; + +public interface MapHost { + boolean isLayerVisible(Key layerID); + void toggleLayer(Key layerID); + + boolean isOverlayVisible(Key overlayID); + void toggleOverlay(Key overlayID); + + MapType getMapType(); + void setMapType(MapType map); +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapScaleDisplay.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapScaleDisplay.java new file mode 100644 index 00000000..15267b73 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/MapScaleDisplay.java @@ -0,0 +1,54 @@ +package com.eerussianguy.blazemap.feature.maps.ui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.engine.render.MapRenderer; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.gui.components.Image; +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.vertex.PoseStack; + +public class MapScaleDisplay extends Image { + private static final ResourceLocation SCALE = BlazeMap.resource("textures/scale.png"); + + private final Font font = Minecraft.getInstance().font; + private final Window window = Minecraft.getInstance().getWindow(); + private final MapRenderer renderer; + private final int size; + + public MapScaleDisplay(int size, MapRenderer renderer) { + super(SCALE, size, size); + this.renderer = renderer; + this.size = size; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + super.render(stack, hasMouse, mouseX, mouseY); + + stack.pushPose(); + double z = renderer.getZoom(); + float w = getWidth(); + float h = getHeight() - font.lineHeight; + + String zoom = z > 1 ? String.format("%.0f : 1", z) : String.format("1 : %.0f", 1 / z); + String distance = String.format("%dm", (int) (size / renderer.getZoom())); + font.draw(stack, zoom, (w-font.width(zoom))/2, h/2 - 8, Colors.NO_TINT); + font.draw(stack, distance, (w-font.width(distance))/2, h/2 + 8, Colors.NO_TINT); + + stack.popPose(); + } + + @Override + public int getWidth() { + return (int) Math.ceil(super.getWidth() / window.getGuiScale()); + } + + @Override + public int getHeight() { + return (int) Math.ceil(super.getHeight() / window.getGuiScale()); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/NamedMapComponentButton.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/NamedMapComponentButton.java new file mode 100644 index 00000000..3ecbcab9 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/NamedMapComponentButton.java @@ -0,0 +1,138 @@ +package com.eerussianguy.blazemap.feature.maps.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.IntConsumer; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.maps.Layer; +import com.eerussianguy.blazemap.api.maps.MapType; +import com.eerussianguy.blazemap.api.maps.NamedMapComponent; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.config.BlazeMapConfig; +import com.eerussianguy.blazemap.config.ServerConfig; +import com.eerussianguy.blazemap.integration.KnownMods; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.ShaderHelper; +import com.eerussianguy.blazemap.lib.gui.components.ImageButton; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.eerussianguy.blazemap.lib.gui.core.TooltipService; +import com.mojang.blaze3d.vertex.PoseStack; + +public abstract class NamedMapComponentButton> extends ImageButton { + private static final ResourceLocation GRAYSCALE = BlazeMap.resource("grayscale"); + + protected final BlazeRegistry.Key key; + protected final MapHost host; + protected final Component name, disabled, owner; + protected final BooleanSupplier active; + protected final ServerConfig.NamedMapComponentPermissions permissions; + protected final ArrayList tooltip = new ArrayList<>(); + + public NamedMapComponentButton(BlazeRegistry.Key key, String type, MapHost host, IntConsumer function, BooleanSupplier active, ServerConfig.NamedMapComponentPermissions permissions) { + super(key.value().getIcon(), 16, 16, function); + this.key = key; + this.host = host; + this.name = key.value().getName().plainCopy(); + this.disabled = Helpers.translate("blazemap.gui.common."+type+".disabled").withStyle(ChatFormatting.DARK_GRAY); + this.owner = new TextComponent(KnownMods.getOwnerName(key)).withStyle(ChatFormatting.BLUE); + this.active = active; + this.permissions = permissions; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + if(active.getAsBoolean() && isAllowed()) { + super.render(stack, hasMouse, mouseX, mouseY); + } else { + ShaderHelper.withTextureShader(GRAYSCALE, () -> { + super.render(stack, hasMouse, mouseX, mouseY); + }); + } + } + + @Override + public boolean isEnabled() { + return isAllowed(); + } + + public boolean isAllowed() { + return permissions.isAllowed(key); + } + + @Override + protected void renderTooltip(PoseStack stack, int mouseX, int mouseY, TooltipService service) { + tooltip.clear(); + populateTooltip(); + tooltip.add(0, name); + tooltip.add(owner); + service.drawTooltip(stack, mouseX, mouseY, tooltip); + } + + protected void populateTooltip() { + if(!isAllowed()) { + tooltip.add(disabled); + } + } + + @Override + protected int getTint() { + return isAllowed() ? (isEnabled() ? Colors.NO_TINT : Colors.DISABLED) : 0xFF333333; + } + + public static class MapTypeButton extends NamedMapComponentButton { + public MapTypeButton(BlazeRegistry.Key key, MapHost host, List> others, BaseComponent own) { + super(key, "map", host, button -> { + host.setMapType(key.value()); + for(var other : others) { + other.setVisible(false); + } + own.setVisible(true); + }, () -> host.getMapType().getID().equals(key), BlazeMapConfig.SERVER.mapPermissions); + } + + @Override + protected boolean onClick(int button) { + if(active.getAsBoolean()){ + playDeniedSound(); + return true; + } + return super.onClick(button); + } + } + + public static class LayerButton extends NamedMapComponentButton { + private final Component bottom = Helpers.translate("blazemap.gui.common.layer.bottom").withStyle(ChatFormatting.DARK_GRAY); + private final boolean isBottom; + + public LayerButton(BlazeRegistry.Key key, MapHost host) { + super(key, "layer", host, button -> host.toggleLayer(key), () -> host.isLayerVisible(key), BlazeMapConfig.SERVER.layerPermissions); + isBottom = key.value().isBottomLayer(); + } + + @Override + protected void populateTooltip() { + super.populateTooltip(); + if(isBottom) tooltip.add(bottom); + } + + @Override + public boolean isEnabled() { + return super.isEnabled() && !isBottom; + } + } + + public static class OverlayButton extends NamedMapComponentButton { + public OverlayButton(BlazeRegistry.Key key, MapHost host) { + super(key, "overlay", host, button -> host.toggleOverlay(key), () -> host.isOverlayVisible(key), BlazeMapConfig.SERVER.overlayPermissions); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/WorldMapDebug.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/WorldMapDebug.java new file mode 100644 index 00000000..21647461 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/WorldMapDebug.java @@ -0,0 +1,76 @@ +package com.eerussianguy.blazemap.feature.maps.ui; + +import java.util.function.BooleanSupplier; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; + +import com.eerussianguy.blazemap.engine.BlazeMapAsync; +import com.eerussianguy.blazemap.engine.render.MapRenderer; +import com.eerussianguy.blazemap.feature.maps.Coordination; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.eerussianguy.blazemap.profiling.Profiler; +import com.eerussianguy.blazemap.profiling.overlay.ProfilingRenderer; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; + +public class WorldMapDebug extends BaseComponent { + private final MapRenderer.DebugInfo debug; + private final Coordination coordination; + private final Profiler.TimeProfiler renderTime, uploadTime; + private final BooleanSupplier visibility; + + public WorldMapDebug(MapRenderer.DebugInfo debug, Coordination coordination, Profiler.TimeProfiler renderTime, Profiler.TimeProfiler uploadTime, BooleanSupplier visibility) { + this.debug = debug; + this.coordination = coordination; + this.renderTime = renderTime; + this.uploadTime = uploadTime; + this.visibility = visibility; + + this.setSize(135, 129); + } + + @Override + public boolean isVisible() { + return visibility.getAsBoolean(); + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + var font = Minecraft.getInstance().font; + RenderHelper.fillRect(stack.last().pose(), getWidth(), getHeight(), Colors.WIDGET_BACKGROUND); + + font.draw(stack, "Debug Info", 5, 5, 0xFFFF0000); + stack.translate(5, 20, 0); + stack.scale(0.5F, 0.5F, 1); + + font.draw(stack, "Atlas Time Profiling:", 0, 0, -1); + var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + ProfilingRenderer.drawTimeProfiler(renderTime, 12, "Render", font, stack.last().pose(), buffers); + ProfilingRenderer.drawTimeProfiler(uploadTime, 24, "Upload", font, stack.last().pose(), buffers); + buffers.endBatch(); + + int y = 30; + font.draw(stack, String.format("Renderer Size: %d x %d", debug.rw, debug.rh), 0, y += 12, -1); + font.draw(stack, String.format("Renderer Zoom: %sx", debug.zoom), 0, y += 12, -1); + font.draw(stack, String.format("Atlas Size: %d x %d", debug.mw, debug.mh), 0, y += 12, -1); + font.draw(stack, String.format("Atlas Frustum: [%d , %d] to [%d , %d]", debug.bx, debug.bz, debug.ex, debug.ez), 0, y += 12, -1); + + font.draw(stack, String.format("Region Matrix: %d x %d", debug.ox, debug.oz), 0, y += 18, -1); + font.draw(stack, String.format("Active Layers: %d", debug.layers), 0, y += 12, -1); + font.draw(stack, String.format("Active Overlays: %d", debug.overlays), 0, y += 12, -1); + font.draw(stack, String.format("Stitching: %s", debug.stitching), 0, y += 12, 0xFF0088FF); + font.draw(stack, String.format("Parallel Pool: %d", BlazeMapAsync.instance().cruncher.poolSize()), 0, y += 12, 0xFFFFFF00); + + font.draw(stack, String.format("Addon Labels: %d", debug.labels), 0, y += 18, -1); + + String region = String.format("Rg %d %d | px: %d %d", coordination.regionX, coordination.regionZ, coordination.regionPixelX, coordination.regionPixelY); + font.draw(stack, region, 0, y += 18, 0x6666FF); + String chunk = String.format("Ch %d %d | px: %d %d", coordination.chunkX, coordination.chunkZ, coordination.chunkPixelX, coordination.chunkPixelY); + font.draw(stack, chunk, 0, y += 12, 0x66FF66); + String block = String.format("Bl %d %d | px: %d %d", coordination.blockX, coordination.blockZ, coordination.blockPixelX, coordination.blockPixelY); + font.draw(stack, block, 0, y += 12, 0xFF6666); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/WorldMapHotkey.java b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/WorldMapHotkey.java new file mode 100644 index 00000000..82606d8a --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/maps/ui/WorldMapHotkey.java @@ -0,0 +1,49 @@ +package com.eerussianguy.blazemap.feature.maps.ui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; + +public class WorldMapHotkey extends BaseComponent { + public static final ResourceLocation KEY = BlazeMap.resource("textures/gui/key.png"); + private static final int KEY_WIDTH = 40; + private static final int KEY_SPACE = 8; + private static final int PADDING = 2; + + private final Component hotkey, description; + private final Font font = Minecraft.getInstance().font; + private final double scale = Minecraft.getInstance().getWindow().getGuiScale() / 2D; + + public WorldMapHotkey(Component hotkey, Component description) { + this.hotkey = hotkey; + this.description = description; + + setSize((int) ((KEY_WIDTH + KEY_SPACE + font.width(description)) / scale), (int) ((font.lineHeight + PADDING * 2) / scale)); + } + + public WorldMapHotkey(String hotkey, String description) { + this(new TextComponent(hotkey), new TextComponent(description)); + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + stack.scale(1F / (float) scale, 1F / (float) scale, 1); + var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + RenderHelper.drawFrame(buffers.getBuffer(RenderType.text(KEY)), stack, KEY_WIDTH, font.lineHeight + PADDING * 2, 4); + buffers.endBatch(); + + font.draw(stack, hotkey, (KEY_WIDTH - font.width(hotkey)) / 2, PADDING, Colors.NO_TINT); + font.draw(stack, description,KEY_WIDTH + KEY_SPACE, PADDING, Colors.NO_TINT); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/overlays/EntityOverlay.java b/src/main/java/com/eerussianguy/blazemap/feature/overlays/EntityOverlay.java new file mode 100644 index 00000000..6ed0da87 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/overlays/EntityOverlay.java @@ -0,0 +1,157 @@ +package com.eerussianguy.blazemap.feature.overlays; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.WaterAnimal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.npc.AbstractVillager; +import net.minecraft.world.entity.player.Player; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.BlazeRegistry; +import com.eerussianguy.blazemap.api.maps.GhostOverlay; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.api.maps.TileResolution; +import com.eerussianguy.blazemap.api.markers.Marker; +import com.eerussianguy.blazemap.lib.Helpers; + +public abstract class EntityOverlay extends GhostOverlay { + private static final ResourceLocation PLAYER = BlazeMap.resource("textures/player.png"); + private static final ArrayList ENTITIES = new ArrayList<>(); + private static final int DEFAULT_ENTITY_MAX_VERTICAL_DISTANCE = 10; + private static final int DEFAULT_PLAYER_MAX_VERTICAL_DISTANCE = -1; + private final int maxVerticalDistance; + + protected EntityOverlay(BlazeRegistry.Key id, TranslatableComponent name, ResourceLocation icon, int maxVerticalDistance) { + super(id, name, icon); + this.maxVerticalDistance = maxVerticalDistance; + } + + protected abstract boolean predicate(Entity entity); + protected abstract int getEntityColor(Entity e); + + protected String getEntityName(Entity e) { + return null; + } + + @Override + public List> getMarkers(ClientLevel level, TileResolution resolution) { + ENTITIES.clear(); + LocalPlayer player = Helpers.getPlayer(); + + for(var entity : level.entitiesForRendering()) { + if(predicate(entity) && (maxVerticalDistance < 0 || Math.abs(player.position().y - entity.position().y) <= maxVerticalDistance)) { + ENTITIES.add(entity); + } + } + + return ENTITIES.stream().map(e -> new EntityMarker(e, getEntityName(e), getEntityColor(e))).collect(Collectors.toList()); + } + + private static class EntityMarker extends Marker { + protected EntityMarker(Entity e, String name, int color) { + super(BlazeMap.resource("entity."+e.getStringUUID()), e.level.dimension(), e.blockPosition(), PLAYER); + setName(name); + setColor(color); + setRotation(e.getRotationVector().y); + } + } + + public static class Players extends EntityOverlay { + public Players() { + super( + BlazeMapReferences.Overlays.PLAYERS, + Helpers.translate("blazemap.players"), + BlazeMap.resource("textures/map_icons/overlay_players.png"), + DEFAULT_PLAYER_MAX_VERTICAL_DISTANCE + ); + } + + @Override + protected boolean predicate(Entity entity) { + return entity instanceof Player && entity != Helpers.getPlayer(); + } + + @Override + protected String getEntityName(Entity e) { + return e.getName().getString(); + } + + @Override + protected int getEntityColor(Entity e) { + return 0xFF88FF66; + } + } + + public static class NPCs extends EntityOverlay { + public NPCs() { + super( + BlazeMapReferences.Overlays.NPCS, + Helpers.translate("blazemap.npcs"), + BlazeMap.resource("textures/map_icons/overlay_npcs.png"), + DEFAULT_ENTITY_MAX_VERTICAL_DISTANCE + ); + } + + @Override + protected boolean predicate(Entity entity) { + return entity instanceof AbstractVillager; + } + + @Override + protected int getEntityColor(Entity e) { + return 0xFFFFFF3F; + } + } + + public static class Animals extends EntityOverlay { + public Animals() { + super( + BlazeMapReferences.Overlays.ANIMALS, + Helpers.translate("blazemap.animals"), + BlazeMap.resource("textures/map_icons/overlay_animals.png"), + DEFAULT_ENTITY_MAX_VERTICAL_DISTANCE + ); + } + + @Override + protected boolean predicate(Entity entity) { + return entity instanceof Animal || entity instanceof WaterAnimal; + } + + @Override + protected int getEntityColor(Entity e) { + return e instanceof WaterAnimal ? 0xFF4488FF : 0xFFA0A0A0; + } + } + + public static class Enemies extends EntityOverlay { + public Enemies() { + super( + BlazeMapReferences.Overlays.ENEMIES, + Helpers.translate("blazemap.enemies"), + BlazeMap.resource("textures/map_icons/overlay_enemies.png"), + DEFAULT_ENTITY_MAX_VERTICAL_DISTANCE + ); + } + + @Override + protected boolean predicate(Entity entity) { + return entity instanceof Monster; + } + + @Override + protected int getEntityColor(Entity e) { + return 0xFFFF2222; + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/overlays/GridOverlay.java b/src/main/java/com/eerussianguy/blazemap/feature/overlays/GridOverlay.java new file mode 100644 index 00000000..de227ac2 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/overlays/GridOverlay.java @@ -0,0 +1,64 @@ +package com.eerussianguy.blazemap.feature.overlays; + +import java.util.EnumMap; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.api.maps.PixelSource; +import com.eerussianguy.blazemap.api.maps.TileResolution; +import com.eerussianguy.blazemap.api.util.RegionPos; +import com.eerussianguy.blazemap.lib.Helpers; + +public class GridOverlay extends Overlay { + private static final EnumMap SOURCES = new EnumMap<>(TileResolution.class); + private static final int REGION = 512; + private static final int CHUNK = 16; + + public GridOverlay() { + super( + BlazeMapReferences.Overlays.GRID, + Helpers.translate("blazemap.grid"), + BlazeMap.resource("textures/map_icons/overlay_grid.png") + ); + } + + @Override + public PixelSource getPixelSource(ResourceKey dimension, RegionPos region, TileResolution resolution) { + return SOURCES.computeIfAbsent(resolution, $ -> new GridPixelSource(resolution)); + } + + private static class GridPixelSource implements PixelSource { + private final TileResolution resolution; + + private GridPixelSource(TileResolution resolution) { + this.resolution = resolution; + } + + @Override + public int getPixel(int x, int y) { + if(isBorder(x, REGION) || isBorder(y, REGION)) return 0x7F0000FF; + if(resolution.pixelWidth > 1) return 0; + + if(isBorder(x, CHUNK) || isBorder(y, CHUNK)) return 0x7FC0C0C0; + return 0; + } + + private boolean isBorder(int param, int type) { + return param % (type / resolution.pixelWidth) == 0; + } + + @Override + public int getWidth() { + return resolution.regionWidth; + } + + @Override + public int getHeight() { + return resolution.regionWidth; + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointEditorFragment.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointEditorFragment.java new file mode 100644 index 00000000..81c5d38d --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointEditorFragment.java @@ -0,0 +1,158 @@ +package com.eerussianguy.blazemap.feature.waypoints; + +import java.util.function.Function; + +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointGroup; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointServiceClient; +import com.eerussianguy.blazemap.lib.*; +import com.eerussianguy.blazemap.lib.gui.components.*; +import com.eerussianguy.blazemap.lib.gui.components.selection.DropdownList; +import com.eerussianguy.blazemap.lib.gui.components.selection.SelectionGrid; +import com.eerussianguy.blazemap.lib.gui.core.VolatileContainer; +import com.eerussianguy.blazemap.lib.gui.fragment.BaseFragment; +import com.eerussianguy.blazemap.lib.gui.fragment.FragmentContainer; + +public class WaypointEditorFragment extends BaseFragment { + private final Waypoint waypoint; + private final boolean creating; + private WaypointGroup defaultGroup = null; + + public WaypointEditorFragment() { + this(Minecraft.getInstance().player.blockPosition()); + } + + public WaypointEditorFragment(BlockPos pos) { + this(pos, null); + } + + public WaypointEditorFragment(BlockPos pos, WaypointGroup defaultGroup) { + this(new Waypoint(BlazeMap.resource("waypoint/"+System.nanoTime()), Minecraft.getInstance().level.dimension(), pos, "New Waypoint", BlazeMapReferences.Icons.WAYPOINT, Colors.randomBrightColor()), true); + this.defaultGroup = defaultGroup; + } + + public WaypointEditorFragment(Waypoint waypoint) { + this(waypoint, false); + } + + private WaypointEditorFragment(Waypoint waypoint, boolean creating) { + super(Helpers.translate("blazemap.gui.waypoint_editor.title")); + this.waypoint = waypoint; + this.creating = creating; + } + + @Override + public void compose(FragmentContainer container, VolatileContainer volatiles) { + int y = 0; + + if(container.titleConsumer.isPresent()) { + container.titleConsumer.get().accept(getTitle()); + } else { + container.add(new Label(getTitle()), 0, y); + y = 15; + } + + + // BASIC INFORMATION =========================================================================================== + container.add(new SectionLabel("Basic Information").setWidth(160), 0, y); + + ObjHolder name = new ObjHolder<>(waypoint.getName()); + container.add(VanillaComponents.makeTextField(font, 160, 14, name), 0, y+=13); + + var pos = waypoint.getPosition(); + IntHolder posX = new IntHolder(pos.getX()), posY = new IntHolder(pos.getY()), posZ = new IntHolder(pos.getZ()); + container.add(VanillaComponents.makeIntField(font, 58, 14, posX), 0 , y += 17); + container.add(VanillaComponents.makeIntField(font, 38, 14, posY), 61 , y); + container.add(VanillaComponents.makeIntField(font, 58, 14, posZ), 102, y); + + DropdownList> dimensions = new DropdownList<>(volatiles, d -> new Label(d.location().toString())); + var dimensionsModel = dimensions.getModel(); + dimensionsModel.setElements(RegistryHelper.getAllDimensions()); + dimensionsModel.setSelected(waypoint.getDimension()); + container.add(dimensions.setSize(160, 14), 0, y += 17); + + DropdownList groups = new DropdownList<>(volatiles, g -> new Label(g.getName())); + var groupsModel = groups.getModel(); + ArraySet groupSet = new ArraySet<>(); + dimensionsModel.addSelectionListener(dimension -> { + groupSet.clear(); + WaypointServiceClient.instance().getPools().forEach(pool -> { + pool.getGroups(dimension).stream().filter(g -> g.management.canCreateChild).forEach(groupSet::add); + }); + groupsModel.setElements(groupSet); + if(groupsModel.getSelected() == null) { + if(defaultGroup != null && groupSet.contains(defaultGroup)) { + groupsModel.setSelected(defaultGroup); + } else { + groupsModel.selectFirst(); + } + } + }); + container.add(groups.setSize(160, 14), 0, y += 17); + + // APPEARANCE ================================================================================================== + container.add(new SectionLabel("Appearance").setWidth(160), 0, y += 22); + IntHolder color = new IntHolder(waypoint.getColor()); + float[] hsb = Colors.RGB2HSB(color.get()); + + SelectionGrid icons = new SelectionGrid<>(Function.identity(), 16, 1, BlazeMapReferences.Icons.ALL_WAYPOINTS).setSize(160, 0).setInitialValue(waypoint.getIcon()); + container.add(icons, 0, y += 13); + + ImageDisplay display = new ImageDisplay().setSize(48, 48).setImageSize(32, 32).setColor(color::get); + icons.setListener(display::setImage); + container.add(display, 0, y += (icons.getHeight() + 3)); + + Slider hue = new HueSlider().setSize(58, 14).setValue(hsb[0]); + container.add(hue, 51, y); + + SBPicker sbPicker = new SBPicker().setSize(48, 48).setHue(hsb[0]).setValue(hsb[1], hsb[2]); + container.add(sbPicker, 112, y); + + Label hex = new Label(String.format("#%06X", color.get() & ~Colors.ALPHA)).setColor(Colors.UNFOCUSED); + container.add(hex, 51, y+= 17); + + TextButton reserved = new TextButton(new TextComponent("Reserved"), $ -> {}).setSize(58, 14); + container.add(reserved, 51, y+= 17); + reserved.setEnabled(false); + reserved.addTooltip(new TextComponent("Reserved for a cool feature later,"), new TextComponent("it does nothing yet.")); + + hue.setListener(h -> { + hsb[0] = h; + color.set(Colors.HSB2RGB(hsb)); + hex.setText(String.format("#%06X", color.get() & ~Colors.ALPHA)); + sbPicker.setHue(h); + }); + + sbPicker.setListener((s, b) -> { + hsb[1] = s; + hsb[2] = b; + color.set(Colors.HSB2RGB(hsb)); + hex.setText(String.format("#%06X", color.get() & ~Colors.ALPHA)); + }); + + + // SUBMIT ====================================================================================================== + TextButton submit = new TextButton(Helpers.translate("blazemap.gui.button.save"), button -> { + waypoint.setName(name.get()); + waypoint.setPosition(new BlockPos(posX.get(), posY.get(), posZ.get())); + waypoint.setDimension(dimensions.getModel().getSelected()); + waypoint.setIcon(icons.getValue()); + waypoint.setColor(color.get()); + WaypointGroup group = groupsModel.getSelected(); + if(creating) { + group.add(waypoint); + } + container.dismiss(); + }); + container.add(submit.setSize(80, 20), 40, y+20); + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointEditorGui.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointEditorGui.java deleted file mode 100644 index d86ea970..00000000 --- a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointEditorGui.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.eerussianguy.blazemap.feature.waypoints; - -import java.awt.*; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.EditBox; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.core.BlockPos; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.phys.Vec3; - -import com.eerussianguy.blazemap.api.BlazeMapReferences; -import com.eerussianguy.blazemap.api.event.DimensionChangedEvent; -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; -import com.eerussianguy.blazemap.api.markers.Waypoint; -import com.eerussianguy.blazemap.gui.*; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; -import com.eerussianguy.blazemap.util.RenderHelper; -import com.mojang.blaze3d.vertex.PoseStack; - -public class WaypointEditorGui extends BlazeGui { - private static IMarkerStorage waypointStorage; - - public static void onDimensionChanged(DimensionChangedEvent event) { - waypointStorage = event.waypoints; - } - - public static void open() { - open(null); - } - - public static void open(Waypoint waypoint) { - Minecraft.getInstance().setScreen(new WaypointEditorGui(waypoint)); - } - - private Button save; - private HueSlider slider; - private SaturationBrightnessSelector sbs; - private final NumericWrapper nx, ny, nz; - - private final Waypoint waypoint; - private ResourceLocation icon = BlazeMapReferences.Icons.WAYPOINT; - private RenderType iconRender; - private String name; - private int x, y, z; - private int color; - private float hue360, s, b; - - protected WaypointEditorGui(Waypoint waypoint) { - super(Helpers.translate("blazemap.gui.waypoint_editor.title"), 212, 202); - this.waypoint = waypoint; - - nx = new NumericWrapper(() -> x, v -> x = v); - ny = new NumericWrapper(() -> y, v -> y = v); - nz = new NumericWrapper(() -> z, v -> z = v); - - if(waypoint == null) { - name = "New Waypoint"; - Vec3 pos = Minecraft.getInstance().player.position(); - x = (int) pos.x; - y = (int) pos.y; - z = (int) pos.z; - color = Colors.randomBrightColor(); - } - else { - name = waypoint.getName(); - BlockPos pos = waypoint.getPosition(); - x = pos.getX(); - y = pos.getY(); - z = pos.getZ(); - color = waypoint.getColor(); - icon = waypoint.getIcon(); - } - - float[] hsb = new float[3]; - int r = (color >> 16) & 0xFF; - int g = (color >> 8) & 0xFF; - int b = color & 0xFF; - Color.RGBtoHSB(r, g, b, hsb); - this.hue360 = hsb[0] * 360; - this.s = hsb[1]; - this.b = hsb[2]; - - iconRender = RenderType.text(icon); - } - - private void createWaypoint() { - if(waypoint == null) { - waypointStorage.add(new Waypoint( - Helpers.identifier("waypoint-" + System.currentTimeMillis()), - getMinecraft().level.dimension(), - new BlockPos(x, y, z), - name, - icon, - color - )); - } - else { - waypoint.setName(name); - waypoint.setPosition(new BlockPos(x, y, z)); - waypoint.setColor(color); - waypoint.setIcon(icon); - // TODO: replace remove + add with a proper changed event - waypointStorage.remove(waypoint); - waypointStorage.add(waypoint); - } - onClose(); - } - - @Override - public void onClose() { - super.onClose(); - if(waypoint != null) { - WaypointManagerGui.open(); - } - } - - private void randomColor() { - hue360 = ((float) System.nanoTime() % 360); - s = 1; - b = 1; - slider.setValue(hue360); - sbs.setHue360(hue360); - sbs.setSB(s, b); - } - - @Override - protected void init() { - super.init(); - - EditBox fname = addRenderableWidget(new EditBox(Minecraft.getInstance().font, left + 12, top + 25, 126, 12, this.title)); - EditBox fx = addRenderableWidget(new EditBox(Minecraft.getInstance().font, left + 12, top + 40, 40, 12, this.title)); - EditBox fy = addRenderableWidget(new EditBox(Minecraft.getInstance().font, left + 55, top + 40, 40, 12, this.title)); - EditBox fz = addRenderableWidget(new EditBox(Minecraft.getInstance().font, left + 98, top + 40, 40, 12, this.title)); - - addRenderableWidget(new SelectionList<>(left + 12, top + 55, 126, 112, 18, this::renderIcon)) - .setItems(BlazeMapReferences.Icons.ALL_WAYPOINTS) - .setSelected(icon) - .setResponder(this::onSelect); - - addRenderableWidget(new Button(left + 12, top + 170, 126, 20, Helpers.translate("blazemap.gui.waypoint_editor.random"), b -> randomColor())); - save = addRenderableWidget(new Button(left + 150, top + 170, 50, 20, Helpers.translate("blazemap.gui.waypoint_editor.save"), b -> createWaypoint())); - - fname.setValue(name); - fx.setValue(String.valueOf(x)); - fy.setValue(String.valueOf(y)); - fz.setValue(String.valueOf(z)); - - sbs = addRenderableWidget(new SaturationBrightnessSelector(left + 150, top + 87, 50, 50)); - sbs.setResponder((s, b) -> { - this.s = s; - this.b = b; - updateColor(); - }); - sbs.setHue360(hue360); - sbs.setSB(s, b); - - slider = addRenderableWidget(new HueSlider(left + 150, top + 140, 50, 20, null, null, 0, 360, 1, 6, 1, false)); - slider.setResponder(hue -> { - sbs.setHue360(hue); - this.hue360 = hue; - updateColor(); - }); - slider.setValue(hue360); - - fname.setResponder(n -> { - name = n; - save.active = n != null && !n.equals(""); - }); - nx.setSubject(fx); - ny.setSubject(fy); - nz.setSubject(fz); - } - - private void updateColor() { - color = Color.HSBtoRGB(hue360 / 360, s, b); - } - - private void onSelect(ResourceLocation icon) { - if(icon == null) icon = BlazeMapReferences.Icons.WAYPOINT; - this.icon = icon; - this.iconRender = RenderType.text(icon); - } - - private void renderIcon(PoseStack stack, ResourceLocation icon) { - RenderHelper.drawTexturedQuad(icon, -1, stack, 2, 1, 16, 16); - String[] path = icon.getPath().split("/"); - font.draw(stack, path[path.length - 1].split("\\.")[0], 20, 5, -1); - } - - @Override - protected void renderComponents(PoseStack stack, MultiBufferSource buffers) { - renderSlot(stack, buffers, 150, 25, 50, 50); - stack.pushPose(); - stack.translate(159, 34, 0); - RenderHelper.drawQuad(buffers.getBuffer(iconRender), stack.last().pose(), 32, 32, color); - stack.popPose(); - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointGroupEditorFragment.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointGroupEditorFragment.java new file mode 100644 index 00000000..a5059323 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointGroupEditorFragment.java @@ -0,0 +1,41 @@ +package com.eerussianguy.blazemap.feature.waypoints; + +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointGroup; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.ObjHolder; +import com.eerussianguy.blazemap.lib.gui.components.Label; +import com.eerussianguy.blazemap.lib.gui.components.TextButton; +import com.eerussianguy.blazemap.lib.gui.components.VanillaComponents; +import com.eerussianguy.blazemap.lib.gui.core.VolatileContainer; +import com.eerussianguy.blazemap.lib.gui.fragment.BaseFragment; +import com.eerussianguy.blazemap.lib.gui.fragment.FragmentContainer; + +public class WaypointGroupEditorFragment extends BaseFragment { + private final WaypointGroup group; + + public WaypointGroupEditorFragment(WaypointGroup group) { + super(Helpers.translate("blazemap.gui.waypoint_group_editor.title"), true, false); + this.group = group; + } + + @Override + public void compose(FragmentContainer container, VolatileContainer volatiles) { + int y = 0; + + if(container.titleConsumer.isPresent()) { + container.titleConsumer.get().accept(getTitle()); + } else { + container.add(new Label(getTitle()), 0, y); + y = 15; + } + + ObjHolder name = new ObjHolder<>(group.getNameString()); + container.add(VanillaComponents.makeTextField(font, 160, 14, name), 0, y); + + TextButton submit = new TextButton(Helpers.translate("blazemap.gui.button.save"), button -> { + group.setUserGivenName(name.get()); + container.dismiss(); + }); + container.add(submit.setSize(80, 20), 40, y+20); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointManagerFragment.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointManagerFragment.java new file mode 100644 index 00000000..ad779d50 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointManagerFragment.java @@ -0,0 +1,117 @@ +package com.eerussianguy.blazemap.feature.waypoints; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.feature.waypoints.service.*; +import com.eerussianguy.blazemap.feature.waypoints.ui.WaypointGroupNode; +import com.eerussianguy.blazemap.integration.KnownMods; +import com.eerussianguy.blazemap.lib.*; +import com.eerussianguy.blazemap.lib.gui.components.IconTabs; +import com.eerussianguy.blazemap.lib.gui.components.Label; +import com.eerussianguy.blazemap.lib.gui.components.Tree; +import com.eerussianguy.blazemap.lib.gui.components.TextButton; +import com.eerussianguy.blazemap.lib.gui.components.selection.DropdownList; +import com.eerussianguy.blazemap.lib.gui.components.selection.SelectionModelSingle; +import com.eerussianguy.blazemap.lib.gui.core.VolatileContainer; +import com.eerussianguy.blazemap.lib.gui.fragment.BaseFragment; +import com.eerussianguy.blazemap.lib.gui.fragment.FragmentContainer; + +public class WaypointManagerFragment extends BaseFragment { + private static final int MANAGER_UI_WIDTH = 200; + + public WaypointManagerFragment() { + super(Helpers.translate("blazemap.gui.waypoint_manager.title"), true, false); + } + + @Override + public void compose(FragmentContainer container, VolatileContainer volatiles) { + WaypointServiceClient client = WaypointServiceClient.instance(); + int y = 0; + + if(container.titleConsumer.isPresent()) { + container.titleConsumer.get().accept(getTitle()); + } else { + container.add(new Label(getTitle()), 0, y); + y = 15; + } + + DropdownList> dimensions = new DropdownList<>(volatiles, d -> new Label(d.location().toString())); + var model = dimensions.getModel(); + model.setElements(RegistryHelper.getAllDimensions()); + model.setSelected(Helpers.levelOrThrow().dimension()); + container.add(dimensions.setSize(MANAGER_UI_WIDTH, 14), 0, y); + + IconTabs tabs = new IconTabs().setSize(MANAGER_UI_WIDTH, 20).setLine(5, 5); + container.add(tabs, 0, y += 17); + + for(var pool : client.getPools()) { + var pc = new PoolContainer(model, container::dismiss, pool); + container.add(pc, 0, y + 25); + tabs.add(pc); + } + } + + private static class PoolContainer extends FragmentContainer implements IconTabs.TabComponent { + private final SelectionModelSingle> dimensions; + private final List tooltip = new ArrayList<>(); + private final WaypointPool pool; + + private PoolContainer(SelectionModelSingle> dimensions, Runnable dismiss, WaypointPool pool) { + super(dismiss, 0); + this.dimensions = dimensions; + this.pool = pool; + tooltip.add(pool.getName()); + tooltip.add(new TextComponent(KnownMods.getOwnerName(pool.id)).withStyle(ChatFormatting.BLUE)); + construct(); + } + + private void construct() { + clear(); + + Tree tree = new Tree().setSize(MANAGER_UI_WIDTH, 160); + add(tree, 0, 0); + dimensions.addSelectionListener(dimension -> { + tree.clearItems(); + var groups = pool.getGroups(dimension); + for(var group : groups) { + tree.addItem(new WaypointGroupNode(group, () -> groups.remove(group))); + } + }); + + var addButton = new TextButton(Helpers.translate("blazemap.gui.button.add_group"), button -> { + pool.getGroups(Helpers.levelOrThrow().dimension()).add(WaypointGroup.make(WaypointChannelLocal.GROUP_DEFAULT)); + PoolContainer.this.construct(); + }).setSize(MANAGER_UI_WIDTH / 2 - 1, 14); + addButton.setEnabled(pool.canUserCreate()); + add(addButton, 0, 162); + } + + @Override + public ResourceLocation getIcon() { + return pool.icon; + } + + @Override + public int getIconTint() { + return pool.tint; + } + + @Override + public Component getName() { + return pool.getName(); + } + + @Override + public List getTooltip() { + return tooltip; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointManagerGui.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointManagerGui.java deleted file mode 100644 index 27cdcfd1..00000000 --- a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointManagerGui.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.eerussianguy.blazemap.feature.waypoints; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.renderer.MultiBufferSource; - -import com.eerussianguy.blazemap.api.event.DimensionChangedEvent; -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; -import com.eerussianguy.blazemap.api.markers.Waypoint; -import com.eerussianguy.blazemap.gui.BlazeGui; -import com.eerussianguy.blazemap.gui.SelectionList; -import com.eerussianguy.blazemap.util.Helpers; -import com.eerussianguy.blazemap.util.RenderHelper; -import com.mojang.blaze3d.vertex.PoseStack; - -public class WaypointManagerGui extends BlazeGui { - private static IMarkerStorage waypointStorage; - - public static void onDimensionChanged(DimensionChangedEvent event) { - waypointStorage = event.waypoints; - } - - public static void open() { - Minecraft.getInstance().setScreen(new WaypointManagerGui()); - } - - private SelectionList list; - private Waypoint selected; - private Button delete, edit; - - protected WaypointManagerGui() { - super(Helpers.translate("blazemap.gui.waypoint_manager.title"), 190, 224); - } - - @Override - protected void init() { - super.init(); - - delete = addRenderableWidget(new Button(left + 12, top + 192, 80, 20, Helpers.translate("blazemap.gui.waypoint_manager.delete"), this::onDelete)); - edit = addRenderableWidget(new Button(left + 98, top + 192, 80, 20, Helpers.translate("blazemap.gui.waypoint_manager.edit"), this::onEdit)); - list = addRenderableWidget(new SelectionList<>(left + 12, top + 25, 166, 162, 20, this::renderWaypoint)).setResponder(this::onSelected).setItems(waypointStorage.getAll().stream().toList()); - updateButtons(); - } - - @Override - protected void renderComponents(PoseStack stack, MultiBufferSource buffers) {} - - private void renderWaypoint(PoseStack stack, Waypoint waypoint) { - RenderHelper.drawTexturedQuad(waypoint.getIcon(), waypoint.getColor(), stack, 2, 2, 16, 16); - font.draw(stack, waypoint.getName(), 20, 6, waypoint.getColor()); - } - - private void onSelected(Waypoint waypoint) { - this.selected = waypoint; - updateButtons(); - } - - private void updateButtons() { - delete.active = edit.active = selected != null; - } - - private void onDelete(Button b) { - waypointStorage.remove(selected); - list.setItems(waypointStorage.getAll().stream().toList()); - } - - private void onEdit(Button b) { - Waypoint waypoint = list.getSelected(); - onClose(); - WaypointEditorGui.open(waypoint); - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointOverlay.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointOverlay.java new file mode 100644 index 00000000..8477b745 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointOverlay.java @@ -0,0 +1,43 @@ +package com.eerussianguy.blazemap.feature.waypoints; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import net.minecraft.client.multiplayer.ClientLevel; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.maps.GhostOverlay; +import com.eerussianguy.blazemap.api.maps.TileResolution; +import com.eerussianguy.blazemap.api.markers.Marker; +import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointServiceClient; +import com.eerussianguy.blazemap.lib.Helpers; + +public class WaypointOverlay extends GhostOverlay { + public WaypointOverlay() { + super( + BlazeMapReferences.Overlays.WAYPOINTS, + Helpers.translate("blazemap.waypoints"), + BlazeMap.resource("textures/map_icons/overlay_waypoints.png") + ); + } + + @Override @SuppressWarnings("unchecked") + public Collection> getMarkers(ClientLevel level, TileResolution resolution) { + WaypointServiceClient waypoints = WaypointServiceClient.instance(); + + if(waypoints == null) { + return Collections.EMPTY_LIST; + } + + ArrayList list = new ArrayList<>(); + waypoints.iterate((waypoint, group) -> { + if(group.getState(waypoint.getID()).isVisible()) { + list.add(waypoint); + } + }); + return Collections.unmodifiableList(list); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointRenderer.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointRenderer.java index f0d5c439..4ee85a32 100644 --- a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointRenderer.java +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointRenderer.java @@ -15,13 +15,13 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.IEventBus; -import com.eerussianguy.blazemap.api.event.DimensionChangedEvent; -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; import com.eerussianguy.blazemap.api.markers.Waypoint; import com.eerussianguy.blazemap.config.BlazeMapConfig; -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.Helpers; -import com.eerussianguy.blazemap.util.RenderHelper; +import com.eerussianguy.blazemap.config.ServerConfig; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointServiceClient; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.RenderHelper; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; @@ -30,16 +30,15 @@ public class WaypointRenderer { - private static IMarkerStorage waypointStorage; - public static void init() { IEventBus bus = MinecraftForge.EVENT_BUS; bus.addListener(WaypointRenderer::onLevelStageRender); - bus.addListener(WaypointRenderer::onDimensionChanged); } public static void onLevelStageRender(RenderLevelStageEvent event) { + if(!BlazeMapConfig.SERVER.mapItemRequirement.canPlayerAccessMap(Helpers.getPlayer(), ServerConfig.MapAccess.READ_LIVE)) return; + // Forge Doc: // Use this to render custom effects into the world, such as custom entity-like objects or special rendering effects. Called within a fabulous graphics target. Happens after entities render. // ForgeRenderTypes.TRANSLUCENT_ON_PARTICLES_TARGET @@ -53,7 +52,7 @@ public static void onLevelStageRender(RenderLevelStageEvent event) { if(playerCamera != null) { Level level = playerCamera.level; - waypointStorage.getAll().forEach(w -> { + WaypointServiceClient.instance().iterate(w -> { final BlockPos pos = w.getPosition(); // TODO: Swap to just using a call to pos.getCenter() where needed in 1.19+ // (method not available in 1.18.2) @@ -152,8 +151,4 @@ private static void translateFromCameraToPos(PoseStack stack, Vec3 pos) { Vec3 cam = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); stack.translate(pos.x() - cam.x(), pos.y() - cam.y(), pos.z() - cam.z()); } - - public static void onDimensionChanged(DimensionChangedEvent event) { - waypointStorage = event.waypoints; - } } diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointStore.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointStore.java deleted file mode 100644 index b3537ada..00000000 --- a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/WaypointStore.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.eerussianguy.blazemap.feature.waypoints; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Supplier; - -import net.minecraft.resources.ResourceLocation; -import net.minecraftforge.common.MinecraftForge; - -import com.eerussianguy.blazemap.BlazeMap; -import com.eerussianguy.blazemap.api.event.ServerJoinedEvent; -import com.eerussianguy.blazemap.api.event.WaypointEvent; -import com.eerussianguy.blazemap.api.markers.IMarkerStorage; -import com.eerussianguy.blazemap.api.markers.Waypoint; -import com.eerussianguy.blazemap.api.util.IOSupplier; -import com.eerussianguy.blazemap.api.util.MinecraftStreams; - -public class WaypointStore implements IMarkerStorage { - private final Map store = new HashMap<>(); - private final Collection view = Collections.unmodifiableCollection(store.values()); - - public static void onServerJoined(ServerJoinedEvent event) { - event.setWaypointStorageFactory(WaypointStore::new); - } - - @Override - public Collection getAll() { - return view; - } - - @Override - public void add(Waypoint waypoint) { - if(store.containsKey(waypoint.getID())) - throw new IllegalStateException("The waypoint is already registered!"); - store.put(waypoint.getID(), waypoint); - MinecraftForge.EVENT_BUS.post(new WaypointEvent.Created(waypoint)); - save(); - } - - @Override - public void remove(ResourceLocation id) { - if(store.containsKey(id)) { - Waypoint waypoint = store.remove(id); - MinecraftForge.EVENT_BUS.post(new WaypointEvent.Removed(waypoint)); - save(); - } - } - - @Override - public boolean has(ResourceLocation id) { - return store.containsKey(id); - } - - - // ================================================================================================================= - private final IOSupplier outputSupplier; - private final IOSupplier inputSupplier; - private final Supplier exists; - - public WaypointStore(IOSupplier inputSupplier, IOSupplier outputSupplier, Supplier exists) { - this.outputSupplier = outputSupplier; - this.inputSupplier = inputSupplier; - this.exists = exists; - load(); - } - - public void save() { - if(outputSupplier == null) { - BlazeMap.LOGGER.warn("Waypoint store storage supplier is null, ignoring save request"); - return; - } - - try(MinecraftStreams.Output output = outputSupplier.get()) { - output.writeInt(store.size()); - for(Waypoint waypoint : store.values()) { - output.writeResourceLocation(waypoint.getID()); - output.writeDimensionKey(waypoint.getDimension()); - output.writeBlockPos(waypoint.getPosition()); - output.writeUTF(waypoint.getName()); - output.writeResourceLocation(waypoint.getIcon()); - output.writeInt(waypoint.getColor()); - output.writeFloat(waypoint.getRotation()); - } - } - catch(IOException e) { - // TODO: Should add some form of retry mechanism to minimise loss of folks' data - BlazeMap.LOGGER.error("Error while saving waypoints. Not all waypoints could be saved. Aborting"); - e.printStackTrace(); - } - } - - public void load() { - if(!exists.get()) return; - try(MinecraftStreams.Input input = inputSupplier.get()) { - int count = input.readInt(); - for(int i = 0; i < count; i++) { - Waypoint waypoint = new Waypoint( - input.readResourceLocation(), - input.readDimensionKey(), - input.readBlockPos(), - input.readUTF(), - input.readResourceLocation(), - input.readInt(), - input.readFloat() - ); - store.put(waypoint.getID(), waypoint); - } - } - catch(IOException e) { - BlazeMap.LOGGER.error("Error loading waypoints. Not all waypoints could be loaded"); - e.printStackTrace(); - } - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/LocalState.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/LocalState.java new file mode 100644 index 00000000..85d6ce2e --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/LocalState.java @@ -0,0 +1,52 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import com.eerussianguy.blazemap.config.BlazeMapConfig; +import com.eerussianguy.blazemap.lib.InheritedBoolean; + +public class LocalState { + public static final LocalState ROOT = new RootLocalState(); + + private LocalState parent; + private InheritedBoolean visibility = InheritedBoolean.DEFAULT; + + public LocalState() { + this(ROOT); + } + + public LocalState(LocalState parent) { + this.parent = parent; + } + + public void setParent(LocalState parent) { + this.parent = parent; + } + + public InheritedBoolean getVisibility() { + return visibility; + } + + public void setVisibility(InheritedBoolean visibility) { + this.visibility = visibility; + } + + public boolean isVisible() { + return visibility.getOrInherit(parent::isVisible); + } + + // ================================================================================================================= + private static final class RootLocalState extends LocalState { + private RootLocalState() { + super(null); + } + + @Override + public InheritedBoolean getVisibility() { + return InheritedBoolean.of(BlazeMapConfig.CLIENT.clientFeatures.displayWaypointsOnMap.get()); + } + + @Override + public boolean isVisible() { + return getVisibility().getOrThrow(); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/ManagementType.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/ManagementType.java new file mode 100644 index 00000000..28b5ddc1 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/ManagementType.java @@ -0,0 +1,17 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +public enum ManagementType { + READONLY(false, false, false, false, false), + INBOX(false, false, true, false, false), + FULL(true, true, true, true, true); + + public final boolean canDelete, canEdit, canDeleteChild, canEditChild, canCreateChild; + + ManagementType(boolean canDelete, boolean canEdit, boolean canDeleteChild, boolean canEditChild, boolean canCreateChild) { + this.canDelete = canDelete; + this.canEdit = canEdit; + this.canDeleteChild = canDeleteChild; + this.canEditChild = canEditChild; + this.canCreateChild = canCreateChild; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannel.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannel.java new file mode 100644 index 00000000..67fae2d8 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannel.java @@ -0,0 +1,28 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import java.util.Collections; +import java.util.List; + +import com.eerussianguy.blazemap.api.BlazeRegistry; + +public abstract class WaypointChannel implements BlazeRegistry.RegistryEntry { + public static final BlazeRegistry REGISTRY = new BlazeRegistry<>(); + + private final BlazeRegistry.Key id; + + protected WaypointChannel(BlazeRegistry.Key id) { + this.id = id; + } + + @Override + public BlazeRegistry.Key getID() { + return id; + } + + public abstract List createClientPools(); + + @SuppressWarnings("unchecked") + public List createServerPools() { + return Collections.EMPTY_LIST; + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannelLocal.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannelLocal.java new file mode 100644 index 00000000..cf25432b --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannelLocal.java @@ -0,0 +1,56 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import java.util.List; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.util.StorageAccess; + +public class WaypointChannelLocal extends WaypointChannel { + public static final ResourceLocation PRIVATE_POOL = BlazeMap.resource("waypoints/pool/private"); + public static final ResourceLocation GROUP_DEFAULT = BlazeMap.resource("waypoint/group/default"); + private static final ResourceLocation GROUP_DEFAULT_FIRST = BlazeMap.resource("waypoint/group/default_first"); + public static final ResourceLocation GROUP_DEATH = BlazeMap.resource("waypoint/group/death"); + public static final Component DEATHS = new TextComponent("Deaths").withStyle(ChatFormatting.YELLOW); + + public WaypointChannelLocal() { + super(REGISTRY.findOrCreate(BlazeMap.resource("local"))); + WaypointGroup.define(GROUP_DEFAULT, () -> new WaypointGroup(GROUP_DEFAULT).setUserGivenName("New Group")); + WaypointGroup.define(GROUP_DEFAULT_FIRST, () -> new WaypointGroup(GROUP_DEFAULT).setUserGivenName("My Waypoints")); + WaypointGroup.define(GROUP_DEATH, () -> new WaypointGroup(GROUP_DEATH, ManagementType.INBOX).setSystemName(DEATHS)); + } + + @Override + public List createClientPools() { + return List.of(new PrivatePool()); + } + + private static class PrivatePool extends WaypointPool { + private static final ResourceLocation LEGACY = BlazeMap.resource("waypoints.bin"); + private static final ResourceLocation PRIVATE = BlazeMap.resource("private.waypoints"); + + private PrivatePool() { + super(PRIVATE_POOL, true, BlazeMapReferences.Icons.WAYPOINT, 0xFF0088FF, new TextComponent("Private")); + } + + @Override + public List getDefaultGroups() { + return List.of(GROUP_DEATH, GROUP_DEFAULT_FIRST); + } + + @Override + public void save(StorageAccess.ServerStorage storage) { + super.save(storage, PRIVATE); + } + + @Override + public void load(StorageAccess.ServerStorage storage) { + super.load(storage, PRIVATE, LEGACY); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannelRemote.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannelRemote.java new file mode 100644 index 00000000..5b82a963 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointChannelRemote.java @@ -0,0 +1,51 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import java.util.List; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.util.StorageAccess; + +public class WaypointChannelRemote extends WaypointChannel { + public static final ResourceLocation SERVER_POOL = BlazeMap.resource("waypoints/pool/server"); + public static final ResourceLocation GROUP = BlazeMap.resource("waypoint/group/server"); + public static final Component PUBLIC_WAYPOINTS = new TextComponent("Public Waypoints").withStyle(ChatFormatting.YELLOW); + + public WaypointChannelRemote() { + super(REGISTRY.findOrCreate(BlazeMap.resource("remote"))); + WaypointGroup.define(GROUP, () -> new WaypointGroup(GROUP, ManagementType.READONLY).setSystemName(PUBLIC_WAYPOINTS)); + } + + @Override + public List createClientPools() { + return List.of(new ServerPool()); + } + + private static class ServerPool extends WaypointPool { + private static final ResourceLocation SERVER = BlazeMap.resource("server.waypoints"); + + private ServerPool() { + super(SERVER_POOL, false, BlazeMapReferences.Icons.INGOT, 0xFFFFFF00, new TextComponent("Server")); + } + + @Override + public List getDefaultGroups() { + return List.of(GROUP); + } + + @Override + public void save(StorageAccess.ServerStorage storage) { + super.save(storage, SERVER); + } + + @Override + public void load(StorageAccess.ServerStorage storage) { + super.load(storage, SERVER); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointGroup.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointGroup.java new file mode 100644 index 00000000..bfb5001a --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointGroup.java @@ -0,0 +1,129 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Objects; +import java.util.function.Supplier; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.api.markers.MarkerStorage; +import com.eerussianguy.blazemap.api.markers.Waypoint; + +public class WaypointGroup implements MarkerStorage { + private static final HashMap> GROUPS = new HashMap<>(); + + public static WaypointGroup make(ResourceLocation type) { + assertDefined(type); + return GROUPS.get(type).get(); + } + + public static void assertDefined(ResourceLocation type) { + if(!GROUPS.containsKey(type)) { + throw new IllegalStateException("WaypointGroup type "+type+" has not been defined"); + } + } + + public static void define(ResourceLocation type, Supplier factory) { + Objects.requireNonNull(type); + Objects.requireNonNull(factory); + if(GROUPS.containsKey(type)) throw new IllegalStateException("Group "+type+" already defined!"); + GROUPS.put(type, factory); + } + + // ================================================================================================================= + public final ResourceLocation type; + public final ManagementType management; + protected final HashMap waypoints = new HashMap<>(); + protected final HashMap states = new HashMap<>(); + private final LocalState state = new LocalState(); + private NameType nameType = NameType.USER_GIVEN; + private Component name; + private String _name; + + public WaypointGroup(ResourceLocation type) { + this(type, ManagementType.FULL); + } + + public WaypointGroup(ResourceLocation type, ManagementType manage) { + assertDefined(type); + this.type = type; + this.management = manage; + } + + public LocalState getState() { + return state; + } + + public LocalState getState(ResourceLocation marker) { + return states.get(marker); + } + + public boolean isUserNamed() { + return nameType == NameType.USER_GIVEN; + } + + public Component getName() { + return name; + } + + public String getNameString() { + return switch(nameType) { + case USER_GIVEN -> _name; + case SYSTEM -> name.getString(); + }; + } + + public WaypointGroup setSystemName(Component name) { + this.name = name; + this._name = null; + this.nameType = NameType.SYSTEM; + return this; + } + + public WaypointGroup setUserGivenName(String name) { + if(nameType == NameType.SYSTEM) { + throw new IllegalStateException("cannot name group with system name"); + } + this._name = name; + this.name = new TextComponent(name); + return this; + } + + @Override + public Collection getAll() { + return waypoints.values(); + } + + public void add(Waypoint marker, LocalState state) { + var key = marker.getID(); + if(waypoints.containsKey(key)) { + throw new IllegalArgumentException("Waypoint Group already contains this waypoint"); + } + waypoints.put(key, marker); + state.setParent(this.state); + states.put(key, state); + } + + @Override + public void add(Waypoint marker) { + add(marker, new LocalState()); + } + + @Override + public void remove(ResourceLocation id) { + waypoints.remove(id); + states.remove(id); + } + + @Override + public boolean has(ResourceLocation id) { + return waypoints.containsKey(id); + } + + private enum NameType { + USER_GIVEN, SYSTEM + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointPool.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointPool.java new file mode 100644 index 00000000..b6001176 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointPool.java @@ -0,0 +1,125 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.api.util.MinecraftStreams; +import com.eerussianguy.blazemap.api.util.StorageAccess; + +public abstract class WaypointPool { + protected final HashMap, List> groups = new HashMap<>(); + public final ResourceLocation id, icon; + public final int tint; + private final Component name; + protected final boolean canUserCreate; + + public WaypointPool(ResourceLocation id, boolean canUserCreate, ResourceLocation icon, int tint, Component name) { + this.id = id; + this.icon = icon; + this.tint = tint; + this.name = name; + this.canUserCreate = canUserCreate; + } + + public Component getName() { + return name; + } + + public boolean canUserCreate() { + return canUserCreate; + } + + public void iterate(ResourceKey dimension, Consumer consumer) { + if(!groups.containsKey(dimension)) return; + + for(WaypointGroup group : groups.get(dimension)) { + group.getAll().forEach(consumer); + } + } + + public void iterate(ResourceKey dimension, BiConsumer consumer) { + if(!groups.containsKey(dimension)) return; + + for(WaypointGroup group : groups.get(dimension)) { + group.getAll().forEach(waypoint -> { + consumer.accept(waypoint, group); + }); + } + } + + public List getGroups(ResourceKey dimension) { + return groups.computeIfAbsent(dimension, $ -> makeDefaultGroups()); + } + + private List makeDefaultGroups() { + return new ArrayList<>( + getDefaultGroups().stream().map(WaypointGroup::make).toList() + ); + } + + @SuppressWarnings("unchecked") + public List getDefaultGroups() { + return Collections.EMPTY_LIST; + } + + public abstract void save(StorageAccess.ServerStorage storage); + public abstract void load(StorageAccess.ServerStorage storage); + + protected void save(StorageAccess.ServerStorage storage, ResourceLocation file) { + try(MinecraftStreams.Output output = storage.write(file)) { + WaypointSerialization.FORMAT.write(groups, output); + } + catch(IOException e) { + // TODO: Should add some form of retry mechanism to minimise loss of folks' data + BlazeMap.LOGGER.error("Error while saving waypoints. Not all waypoints could be saved. Aborting"); + e.printStackTrace(); + } + } + + protected void load(StorageAccess.ServerStorage storage, ResourceLocation file) { + this.load(storage, file, null); + } + + protected void load(StorageAccess.ServerStorage storage, ResourceLocation file, @Nullable ResourceLocation legacy) { + groups.clear(); + + // Load standard format + if(storage.exists(file)) { + try(MinecraftStreams.Input input = storage.read(file)) { + groups.putAll(WaypointSerialization.FORMAT.read(input)); + } + catch(IOException e) { + BlazeMap.LOGGER.error("Error loading waypoints. Waypoints could not be loaded.", e); + } + return; // standard file exists, don't fall back to legacy loading + } + + // Attempt to load legacy format, if provided + if(legacy == null) return; + storage.forEachLevel((key, _storage) -> { + if(!_storage.exists(legacy)) return; + try(MinecraftStreams.Input input = _storage.read(legacy)) { + groups.putAll(WaypointSerialization.FORMAT.readLegacy(input)); + } + catch(IOException e) { + BlazeMap.LOGGER.error("Error loading legacy waypoints for level \""+key+"\". Not all waypoints could be loaded", e); + } + }); + + save(storage, file); // Legacy waypoints were loaded. Saving immediately forces standard file to exist from the start. + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointSerialization.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointSerialization.java new file mode 100644 index 00000000..346bb06a --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointSerialization.java @@ -0,0 +1,122 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.api.util.MinecraftStreams; +import com.eerussianguy.blazemap.lib.InheritedBoolean; +import com.eerussianguy.blazemap.lib.RegistryHelper; +import com.eerussianguy.blazemap.lib.io.FormatSpec; +import com.eerussianguy.blazemap.lib.io.Format; +import com.eerussianguy.blazemap.lib.io.FormatVersion; + +public class WaypointSerialization { + public static final Format, List>> FORMAT + = new FormatSpec<>((byte) 0x00, new CurrentFormat()) + .setLegacyLoader(OutdatedFormats.legacy) + .freeze(); + + + private static class CurrentFormat implements FormatVersion, List>> { + @Override + public void write(Map, List> data, MinecraftStreams.Output output) throws IOException { + // Write non-empty dimensions + output.writeCollection(data.entrySet().stream().filter(e -> e.getValue().size() > 0).toList(), entry -> { + output.writeResourceLocation(entry.getKey().location()); + + // Write groups in dimension + output.writeCollection(entry.getValue(), group -> { + output.writeResourceLocation(group.type); + if(group.isUserNamed()) { + output.writeUTF(group.getNameString()); + } + output.writeByte(group.getState().getVisibility().ordinal()); + + // Write waypoints in group + output.writeCollection(group.getAll(), waypoint -> { + output.writeResourceLocation(waypoint.getID()); + output.writeBlockPos(waypoint.getPosition()); + output.writeUTF(waypoint.getName()); + output.writeResourceLocation(waypoint.getIcon()); + output.writeInt(waypoint.getColor()); + output.writeFloat(waypoint.getRotation()); + output.writeByte(group.getState(waypoint.getID()).getVisibility().ordinal()); + }); + }); + }); + } + + @Override + public Map, List> read(MinecraftStreams.Input input) throws IOException { + Map, List> data = new HashMap<>(); + + // Read non-empty dimensions + input.readCollection(() -> { + ResourceKey dimension = RegistryHelper.getDimension(input.readResourceLocation()); + ArrayList groups = new ArrayList<>(); + data.put(dimension, groups); + + // Read groups in dimension + input.readCollection(() -> { + WaypointGroup group = WaypointGroup.make(input.readResourceLocation()); + if(group.isUserNamed()) { + group.setUserGivenName(input.readUTF()); + } + group.getState().setVisibility(InheritedBoolean.values()[input.readByte()]); + groups.add(group); + + // Read waypoints in group + input.readCollection(() -> { + ResourceLocation waypointID = input.readResourceLocation(); + group.add(new Waypoint( + waypointID, + dimension, + input.readBlockPos(), + input.readUTF(), + input.readResourceLocation(), + input.readInt()) + .setRotation(input.readFloat()) + ); + group.getState(waypointID).setVisibility(InheritedBoolean.values()[input.readByte()]); + }); + }); + }); + + return data; + } + } + + private static class OutdatedFormats { + private static final FormatVersion.Outdated, List>> legacy = input -> { + Map, List> data = new HashMap<>(); + input.readCollection(() -> { + Waypoint waypoint = new Waypoint( + input.readResourceLocation(), + readDeprecatedLegacyDimension(input), + input.readBlockPos(), + input.readUTF(), + input.readResourceLocation(), + input.readInt() + ).setRotation(input.readFloat()); + data.computeIfAbsent(waypoint.getDimension(), $ -> new ArrayList<>(List.of(new WaypointGroup(WaypointChannelLocal.GROUP_DEFAULT)))).get(0).add(waypoint); + }); + return data; + }; + + /** @deprecated storing the registry name is stupid and ResourceLocation suffices */ + @Deprecated(since="1.18.2-0.8.0", forRemoval = true) + private static ResourceKey readDeprecatedLegacyDimension(MinecraftStreams.Input input) throws IOException { + ResourceLocation registry = input.readResourceLocation(); + ResourceLocation location = input.readResourceLocation(); + return ResourceKey.create(ResourceKey.createRegistryKey(registry), location); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointService.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointService.java new file mode 100644 index 00000000..1a26d520 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointService.java @@ -0,0 +1,78 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import net.minecraftforge.fml.LogicalSide; + +import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.api.util.StorageAccess; + +public abstract class WaypointService { + public static void init() { + WaypointChannel.REGISTRY.register(new WaypointChannelLocal()); + WaypointChannel.REGISTRY.register(new WaypointChannelRemote()); + } + + // ================================================================================================================= + protected final StorageAccess.ServerStorage storage; + private final HashMap pools = new HashMap<>(); + private final List view; + + protected WaypointService(LogicalSide side, StorageAccess.ServerStorage storage) { + this.storage = storage; + List created = new ArrayList<>(); + for(var key : WaypointChannel.REGISTRY.keys()) { + var channel = key.value(); + created.addAll(switch(side) { + case CLIENT -> channel.createClientPools(); + case SERVER -> channel.createServerPools(); + }); + } + for(var pool : created) { + pools.put(pool.id, pool); + } + view = Collections.unmodifiableList(created); + } + + public void iterate(ResourceKey level, Consumer consumer) { + for(WaypointPool pool : view) { + pool.iterate(level, consumer); + } + } + + public void iterate(ResourceKey level, BiConsumer consumer) { + for(WaypointPool pool : view) { + pool.iterate(level, consumer); + } + } + + public List getPools() { + return view; + } + + public WaypointPool getPool(ResourceLocation id) { + return pools.get(id); + } + + protected void shutdown() { + save(); + } + + protected void save() { + for(var pool : view) { + pool.save(storage); + } + } + + protected void load() { + for(var pool : view) { + pool.load(storage); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointServiceClient.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointServiceClient.java new file mode 100644 index 00000000..84dd4be3 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointServiceClient.java @@ -0,0 +1,67 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.event.entity.EntityLeaveWorldEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.LogicalSide; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.event.ClientEngineEvent; +import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.api.util.StorageAccess; +import com.eerussianguy.blazemap.config.BlazeMapConfig; +import com.eerussianguy.blazemap.lib.Helpers; + +public class WaypointServiceClient extends WaypointService { + private static final ResourceLocation DEATH = BlazeMap.resource("textures/waypoints/special/death.png"); + private static WaypointServiceClient client = null; + + public static WaypointServiceClient instance() { + return client; + } + + @SubscribeEvent + public static void onClientEngineStarting(ClientEngineEvent.EngineStartingEvent event) { + client = new WaypointServiceClient(event.serverStorage); + } + + @SubscribeEvent + public static void onClientEngineStopping(ClientEngineEvent.EngineStoppingEvent event) { + client.shutdown(); + client = null; + } + + @SubscribeEvent + public static void onDeath(EntityLeaveWorldEvent event) { + if(!BlazeMapConfig.CLIENT.clientFeatures.deathWaypoints.get()) return; + var entity = event.getEntity(); + if(!entity.level.isClientSide) return; + if(entity instanceof LocalPlayer player) { + if(!player.isDeadOrDying()) return; + + var dimension = player.level.dimension(); + Waypoint waypoint = new Waypoint(BlazeMap.resource("waypoint/death/"+System.nanoTime()), dimension, player.blockPosition(), Helpers.getISO8601('-', ' ', ':'), DEATH); + client.getPool(WaypointChannelLocal.PRIVATE_POOL).getGroups(dimension) + .stream().filter(g -> g.type == WaypointChannelLocal.GROUP_DEATH).findFirst() + .ifPresent(g -> g.add(waypoint)); + } + } + + // ================================================================================================================= + public WaypointServiceClient(StorageAccess.ServerStorage storage) { + super(LogicalSide.CLIENT, storage); + load(); + } + + public void iterate(Consumer consumer) { + iterate(Helpers.levelOrThrow().dimension(), consumer); + } + + public void iterate(BiConsumer consumer) { + iterate(Helpers.levelOrThrow().dimension(), consumer); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointServiceServer.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointServiceServer.java new file mode 100644 index 00000000..b1cfb74f --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/service/WaypointServiceServer.java @@ -0,0 +1,32 @@ +package com.eerussianguy.blazemap.feature.waypoints.service; + +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.LogicalSide; + +import com.eerussianguy.blazemap.api.event.ServerEngineEvent; +import com.eerussianguy.blazemap.api.util.StorageAccess; + +public class WaypointServiceServer extends WaypointService { + private static WaypointServiceServer server = null; + + public static WaypointServiceServer instance() { + return server; + } + + @SubscribeEvent + public static void onServerEngineStarting(ServerEngineEvent.EngineStartingEvent event) { + server = new WaypointServiceServer(event.serverStorage); + } + + @SubscribeEvent + public static void onServerEngineStopping(ServerEngineEvent.EngineStoppingEvent event) { + server.shutdown(); + server = null; + } + + // ================================================================================================================= + + protected WaypointServiceServer(StorageAccess.ServerStorage storage) { + super(LogicalSide.SERVER, storage); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointGroupNode.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointGroupNode.java new file mode 100644 index 00000000..47e93223 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointGroupNode.java @@ -0,0 +1,117 @@ +package com.eerussianguy.blazemap.feature.waypoints.ui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.feature.waypoints.WaypointEditorFragment; +import com.eerussianguy.blazemap.feature.waypoints.WaypointGroupEditorFragment; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointGroup; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.components.Tree; +import com.eerussianguy.blazemap.lib.gui.core.ContainerAnchor; +import com.eerussianguy.blazemap.lib.gui.core.EdgeReference; +import com.eerussianguy.blazemap.lib.gui.core.TooltipService; +import com.mojang.blaze3d.vertex.PoseStack; + +public class WaypointGroupNode extends WaypointTreeNode { + private final WaypointGroup group; + private final ArrayList children; + private final EdgeReference add = new EdgeReference(this, ContainerAnchor.TOP_RIGHT).setSize(8, 8).setPosition(32, 2); + private boolean open = true; + + public WaypointGroupNode(WaypointGroup group, Runnable delete) { + super(group.getName(), group.getState(), delete, "blazemap.gui.button.edit_group"); + this.group = group; + this.children = new ArrayList<>(group.getAll().stream().map(this::makeChild).toList()); + } + + private WaypointLeaf makeChild(Waypoint waypoint) { + ResourceLocation id = waypoint.getID(); + return new WaypointLeaf( + waypoint, + group, + () -> { + group.remove(id); + } + ); + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + renderFlatBackground(stack, 0xFF444444); + if(group.management.canCreateChild) { + RenderHelper.drawTexturedQuad(ADD, Colors.NO_TINT, stack, add.getPositionX(), add.getPositionY(), add.getWidth(), add.getHeight()); + } + stack.pushPose(); + stack.translate(getHeight(), getHeight() / 2F - 4, 0); + font.draw(stack, open ? "v" : ">", -9, 1, Colors.BLACK); + stack.popPose(); + super.render(stack, hasMouse, mouseX, mouseY); + } + + @Override + @SuppressWarnings("unchecked") + public List getChildren() { + if(open) { + children.removeIf(Tree.TreeItem::wasDeleted); + return (List) children; + } + else { + return Collections.EMPTY_LIST; + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if(super.mouseClicked(mouseX, mouseY, button)) return true; + + if(group.management.canCreateChild && add.mouseIntercepts(mouseX, mouseY)) { + boolean opened = new WaypointEditorFragment(Helpers.getPlayer().blockPosition(), group).push(updater); + if(opened) { + playOkSound(); + } + else { + playDeniedSound(); + } + return true; + } + + this.open = !this.open; + updater.run(); + + if(open) playUpSound(); + else playDownSound(); + + return true; + } + + @Override + protected boolean isDeletable() { + return group.management.canDelete; + } + + @Override + protected boolean isEditable() { + return group.management.canEdit; + } + + @Override + protected boolean editNode() { + return new WaypointGroupEditorFragment(group).push(); + } + + @Override + protected void renderTooltip(PoseStack stack, int mouseX, int mouseY, TooltipService service) { + if(group.management.canCreateChild && add.mouseIntercepts(mouseX, mouseY)) { + service.drawTooltip(stack, mouseX, mouseY, Helpers.translate("blazemap.gui.button.add_waypoint")); + return; + } + super.renderTooltip(stack, mouseX, mouseY, service); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointLeaf.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointLeaf.java new file mode 100644 index 00000000..df460c10 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointLeaf.java @@ -0,0 +1,40 @@ +package com.eerussianguy.blazemap.feature.waypoints.ui; + +import com.eerussianguy.blazemap.api.markers.Waypoint; +import com.eerussianguy.blazemap.feature.waypoints.WaypointEditorFragment; +import com.eerussianguy.blazemap.feature.waypoints.service.WaypointGroup; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.mojang.blaze3d.vertex.PoseStack; + +public class WaypointLeaf extends WaypointTreeNode { + private static final int ICON_SIZE = 8; + + private final Waypoint waypoint; + private final WaypointGroup group; + + public WaypointLeaf(Waypoint waypoint, WaypointGroup group, Runnable delete) { + super(waypoint.getName(), group.getState(waypoint.getID()), delete, "blazemap.gui.button.edit_waypoint"); + this.waypoint = waypoint; + this.group = group; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + int offset = (getHeight() - ICON_SIZE) / 2; + if(hasMouse && mouseIntercepts(mouseX, mouseY)) { + renderFlatBackground(stack, 0xFF222222); // render hover + } + RenderHelper.drawTexturedQuad(waypoint.getIcon(), waypoint.getColor(), stack, offset, offset, ICON_SIZE, ICON_SIZE); + super.render(stack, hasMouse, mouseX, mouseY); + } + + @Override + protected boolean editNode() { + return new WaypointEditorFragment(waypoint).push(); + } + + @Override + protected boolean isEditable() { + return group.management.canEditChild; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointTreeNode.java b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointTreeNode.java new file mode 100644 index 00000000..7caae420 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/feature/waypoints/ui/WaypointTreeNode.java @@ -0,0 +1,181 @@ +package com.eerussianguy.blazemap.feature.waypoints.ui; + +import org.lwjgl.glfw.GLFW; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.feature.waypoints.service.LocalState; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; +import com.eerussianguy.blazemap.lib.InheritedBoolean; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.components.Label; +import com.eerussianguy.blazemap.lib.gui.components.Tree; +import com.eerussianguy.blazemap.lib.gui.core.ContainerAnchor; +import com.eerussianguy.blazemap.lib.gui.core.EdgeReference; +import com.eerussianguy.blazemap.lib.gui.core.TooltipService; +import com.eerussianguy.blazemap.lib.gui.trait.BorderedComponent; +import com.eerussianguy.blazemap.lib.gui.trait.ComponentSounds; +import com.mojang.blaze3d.vertex.PoseStack; + +public abstract class WaypointTreeNode extends Label implements Tree.TreeItem, BorderedComponent, ComponentSounds, GuiEventListener { + protected static final ResourceLocation ADD = BlazeMap.resource("textures/gui/add.png"); + protected static final ResourceLocation REMOVE = BlazeMap.resource("textures/gui/remove.png"); + protected static final ResourceLocation EDIT = BlazeMap.resource("textures/gui/edit.png"); + private static final ResourceLocation ON_OVERRIDE = BlazeMap.resource("textures/gui/on.png"); + private static final ResourceLocation ON_INHERITED = BlazeMap.resource("textures/gui/on_inherited.png"); + private static final ResourceLocation OFF_OVERRIDE = BlazeMap.resource("textures/gui/off.png"); + private static final ResourceLocation OFF_INHERITED = BlazeMap.resource("textures/gui/off_inherited.png"); + + protected final EdgeReference visibility = new EdgeReference(this, ContainerAnchor.TOP_RIGHT).setSize(8, 8).setPosition(2, 2); + protected final EdgeReference delete = new EdgeReference(this, ContainerAnchor.TOP_RIGHT).setSize(8, 8).setPosition(12, 2); + protected final EdgeReference edit = new EdgeReference(this, ContainerAnchor.TOP_RIGHT).setSize(8, 8).setPosition(22, 2); + protected final LocalState state; + protected final Runnable onDelete; + protected final String editTooltip; + protected Runnable updater = () -> {}; + private boolean wasDeleted = false; + + protected WaypointTreeNode(Component text, LocalState state, Runnable delete, String editTooltip) { + super(text); + this.state = state; + this.onDelete = delete; + this.editTooltip = editTooltip; + } + + protected WaypointTreeNode(String text, LocalState state, Runnable delete, String editTooltip) { + super(text); + this.state = state; + this.onDelete = delete; + this.editTooltip = editTooltip; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + ResourceLocation texture = switch(state.getVisibility()) { + case TRUE -> ON_OVERRIDE; + case FALSE -> OFF_OVERRIDE; + case DEFAULT -> state.isVisible() ? ON_INHERITED : OFF_INHERITED; + }; + RenderHelper.drawTexturedQuad(texture, Colors.NO_TINT, stack, visibility.getPositionX(), visibility.getPositionY(), visibility.getWidth(), visibility.getHeight()); + if(isDeletable()) { + RenderHelper.drawTexturedQuad(REMOVE, Screen.hasShiftDown() ? Colors.NO_TINT : Colors.DISABLED, stack, delete.getPositionX(), delete.getPositionY(), delete.getWidth(), delete.getHeight()); + } + if(isEditable()) { + RenderHelper.drawTexturedQuad(EDIT, Colors.NO_TINT, stack, edit.getPositionX(), edit.getPositionY(), edit.getWidth(), edit.getHeight()); + } + + stack.translate(getHeight(), getHeight() / 2F - 4, 0); + super.render(stack, hasMouse, mouseX, mouseY); + } + + @Override + protected void renderTooltip(PoseStack stack, int mouseX, int mouseY, TooltipService service) { + if(visibility.mouseIntercepts(mouseX, mouseY)) { + InheritedBoolean visible = state.getVisibility(); + var tooltip = Helpers.translate(switch(visible) { + case TRUE -> "blazemap.gui.button.visibility_show"; + case FALSE -> "blazemap.gui.button.visibility_hide"; + case DEFAULT -> "blazemap.gui.button.visibility_default"; + }); + service.drawTooltip(stack, mouseX, mouseY, tooltip); + return; + } + + if(isDeletable() && delete.mouseIntercepts(mouseX, mouseY)) { + var tooltip = Helpers.translate("blazemap.gui.button.delete"); + if(!Screen.hasShiftDown()) { + service.drawTooltip(stack, mouseX, mouseY, tooltip, Helpers.translate("blazemap.gui.tooltip.confirm_delete").withStyle(ChatFormatting.YELLOW)); + } + else { + service.drawTooltip(stack, mouseX, mouseY, tooltip.withStyle(ChatFormatting.RED)); + } + return; + } + + if(isEditable() && edit.mouseIntercepts(mouseX, mouseY)) { + service.drawTooltip(stack, mouseX, mouseY, Helpers.translate(editTooltip)); + } + } + + protected boolean isDeletable() { + return true; + } + + protected boolean isEditable() { + return true; + } + + @Override + public int getColor() { + return state.isVisible() ? Colors.WHITE : Colors.DISABLED; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if(visibility.mouseIntercepts(mouseX, mouseY)) { + int direction = switch(button) { + case GLFW.GLFW_MOUSE_BUTTON_1 -> 1; + case GLFW.GLFW_MOUSE_BUTTON_2 -> -1; + default -> 0; + }; + if(direction == 0) { + playDeniedSound(); + } + else { + playOkSound(); + state.setVisibility(Helpers.cycle(state.getVisibility(), direction)); + } + return true; + } + if(isDeletable() && delete.mouseIntercepts(mouseX, mouseY)) { + if(Screen.hasShiftDown()) { + playOkSound(); + onDelete.run(); + wasDeleted = true; + updater.run(); + } + else { + playDeniedSound(); + } + return true; + } + if(isEditable() && edit.mouseIntercepts(mouseX, mouseY)) { + boolean editing = editNode(); + if(editing) { + playOkSound(); + } + else { + playDeniedSound(); + } + return true; + } + return false; + } + + @Override + public boolean wasDeleted() { + return wasDeleted; + } + + protected abstract boolean editNode(); + + @Override + public void setUpdater(Runnable function) { + this.updater = function; + } + + @Override + public int getWidth() { + return getParent().getWidth(); + } + + @Override + public int getTextHeight() { + return 12; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/gui/HueSlider.java b/src/main/java/com/eerussianguy/blazemap/gui/HueSlider.java deleted file mode 100644 index 1612ca81..00000000 --- a/src/main/java/com/eerussianguy/blazemap/gui/HueSlider.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.eerussianguy.blazemap.gui; - -import java.util.function.Consumer; - -import net.minecraft.network.chat.Component; -import net.minecraftforge.client.gui.widget.ForgeSlider; - -public class HueSlider extends ForgeSlider { - private Consumer responder; - - public HueSlider(int x, int y, int width, int height, Component prefix, Component suffix, double minValue, double maxValue, double currentValue, double stepSize, int precision, boolean drawString) { - super(x, y, width, height, prefix, suffix, minValue, maxValue, currentValue, stepSize, precision, drawString); - } - - public HueSlider(int x, int y, int width, int height, Component prefix, Component suffix, double minValue, double maxValue, double currentValue, boolean drawString) { - super(x, y, width, height, prefix, suffix, minValue, maxValue, currentValue, drawString); - } - - public void setResponder(Consumer responder) { - this.responder = responder; - } - - @Override - protected void applyValue() { - if(this.responder != null) { - responder.accept((float) this.value * 360); - } - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/gui/Image.java b/src/main/java/com/eerussianguy/blazemap/gui/Image.java deleted file mode 100644 index 059f20b7..00000000 --- a/src/main/java/com/eerussianguy/blazemap/gui/Image.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.eerussianguy.blazemap.gui; - -import net.minecraft.client.gui.components.Widget; -import net.minecraft.resources.ResourceLocation; - -import com.eerussianguy.blazemap.util.Colors; -import com.eerussianguy.blazemap.util.RenderHelper; -import com.mojang.blaze3d.vertex.PoseStack; - -public class Image implements Widget { - private final int posX, posY, width, height; - private final ResourceLocation image; - private int color = Colors.NO_TINT; - - public Image(ResourceLocation image, int posX, int posY, int width, int height) { - this.posX = posX; - this.posY = posY; - this.width = width; - this.height = height; - this.image = image; - } - - public Image color(int color) { - this.color = color; - return this; - } - - @Override - public void render(PoseStack stack, int i0, int i1, float f0) { - RenderHelper.drawTexturedQuad(image, color, stack, posX, posY, width, height); - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/gui/NumericWrapper.java b/src/main/java/com/eerussianguy/blazemap/gui/NumericWrapper.java deleted file mode 100644 index e320b7df..00000000 --- a/src/main/java/com/eerussianguy/blazemap/gui/NumericWrapper.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.eerussianguy.blazemap.gui; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -import net.minecraft.client.gui.components.EditBox; - -public class NumericWrapper { - private final Supplier source; - private final Consumer target; - - public NumericWrapper(Supplier source, Consumer target) { - this.source = source; - this.target = target; - } - - public void setSubject(EditBox subject) { - subject.setResponder(s -> { - if(s == null || s.equals("") || s.equals("-")) return; - try { - int v = Integer.parseInt(s); - target.accept(v); - } - catch(NumberFormatException ex) { - subject.setValue(String.valueOf(source.get())); - } - }); - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/gui/SaturationBrightnessSelector.java b/src/main/java/com/eerussianguy/blazemap/gui/SaturationBrightnessSelector.java deleted file mode 100644 index 66632293..00000000 --- a/src/main/java/com/eerussianguy/blazemap/gui/SaturationBrightnessSelector.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.eerussianguy.blazemap.gui; - -import java.awt.*; -import java.util.function.BiConsumer; - -import net.minecraft.client.gui.components.Widget; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.renderer.LightTexture; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; - -import com.eerussianguy.blazemap.util.RenderHelper; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.Tesselator; -import com.mojang.blaze3d.vertex.VertexConsumer; -import com.mojang.math.Matrix4f; - -public class SaturationBrightnessSelector implements Widget, GuiEventListener, NarratableEntry { - private final int x, y, w, h; - private float hue, s, b; - private BiConsumer responder; - - public SaturationBrightnessSelector(int x, int y, int w, int h) { - this.x = x; - this.y = y; - this.w = w; - this.h = h; - } - - @Override - public void render(PoseStack stack, int mx, int my, float p) { - stack.pushPose(); - stack.translate(x, y, 0); - var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); - VertexConsumer slot = buffers.getBuffer(RenderType.text(BlazeGui.SLOT)); - RenderHelper.drawFrame(slot, stack, w, h, 1); - - Matrix4f matrix = stack.last().pose(); - VertexConsumer vertices = buffers.getBuffer(RenderHelper.SOLID); - int color = Color.HSBtoRGB(hue / 360, 1, 1); - vertices.vertex(matrix, 1.0F, h - 1, -0.01F).color(0xFF000000).uv(0, 1).uv2(LightTexture.FULL_BRIGHT).endVertex(); - vertices.vertex(matrix, w - 1, h - 1, -0.01F).color(0xFF000000).uv(1, 1).uv2(LightTexture.FULL_BRIGHT).endVertex(); - vertices.vertex(matrix, w - 1, 1.0F, -0.01F).color(color).uv(1, 0).uv2(LightTexture.FULL_BRIGHT).endVertex(); - vertices.vertex(matrix, 1.0F, 1.0F, -0.01F).color(0xFFFFFFFF).uv(0, 0).uv2(LightTexture.FULL_BRIGHT).endVertex(); - - buffers.endBatch(); - stack.popPose(); - } - - public void setHue360(float hue) { - this.hue = hue; - } - - public void setResponder(BiConsumer responder) { - this.responder = responder; - } - - public void setSB(float s, float b) { - this.s = s; - this.b = b; - if(responder != null) { - responder.accept(s, b); - } - } - - @Override - public boolean mouseClicked(double mx, double my, int b) { - mx -= x; - my -= y; - if(mx <= 1 || mx >= w - 1 || my <= 1 || my >= h - 1) return false; - int rw = w - 2; - int rh = h - 2; - mx--; - my--; - setSB((float) mx / rw, 1F - ((float) my / rh)); - return true; - } - - @Override - public NarrationPriority narrationPriority() {return NarrationPriority.NONE;} - - @Override - public void updateNarration(NarrationElementOutput p_169152_) {} -} diff --git a/src/main/java/com/eerussianguy/blazemap/gui/SelectionList.java b/src/main/java/com/eerussianguy/blazemap/gui/SelectionList.java deleted file mode 100644 index c0d6e2e4..00000000 --- a/src/main/java/com/eerussianguy/blazemap/gui/SelectionList.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.eerussianguy.blazemap.gui; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -import net.minecraft.client.gui.components.Widget; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; - -import com.eerussianguy.blazemap.util.Helpers; -import com.eerussianguy.blazemap.util.RenderHelper; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.Tesselator; - -public class SelectionList implements Widget, GuiEventListener, NarratableEntry { - private final int x, y, w, h, rh, iw, ih, bx, by; - private final EntryRenderer renderer; - private Consumer responder = w -> {}; - private List items = Collections.EMPTY_LIST; - - private int selected = -1, begin = 0; - private int maxDisplay, maxBegin; - private float scroll = 0F; - - public SelectionList(int x, int y, int w, int h, int rh, EntryRenderer renderer) { - this.x = x; - this.y = y; - this.w = w; - this.h = h; - this.rh = rh; - this.iw = w - 5; - this.ih = h - 2; - this.bx = x + 1; - this.by = y + 1; - this.renderer = renderer; - } - - public SelectionList setItems(List items) { - this.items = items; - this.maxDisplay = Math.min(ih / rh, items.size()); - this.selected = -1; - this.maxBegin = items.size() - maxDisplay; - this.begin = Math.min(begin, maxBegin); - if(items.size() <= maxDisplay) { - scroll = 0F; - } - else { - scroll = ((float) maxDisplay) / ((float) items.size()) * rh; - } - responder.accept(null); - return this; - } - - public SelectionList setResponder(Consumer responder) { - this.responder = Objects.requireNonNull(responder); - return this; - } - - public T getSelected() { - if(selected < 0 || selected >= items.size()) return null; - return items.get(selected); - } - - public SelectionList setSelected(T item) { - int previous = selected; - selected = items.indexOf(item); - if(selected != previous) { - responder.accept(getSelected()); - } - return this; - } - - @Override - public void render(PoseStack stack, int mx, int my, float p) { - stack.pushPose(); - stack.translate(x, y, 0); - - var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); - RenderHelper.drawFrame(buffers.getBuffer(RenderType.text(BlazeGui.SLOT)), stack, w, h, 1); - - stack.pushPose(); - stack.translate(1 + iw, 1, 0); - RenderHelper.fillRect(buffers, stack.last().pose(), 3, ih, 0x80000000); - if(scroll > 0F) { - stack.translate(0, begin * scroll, 1); - RenderHelper.fillRect(buffers, stack.last().pose(), 3, ((float) maxDisplay) * scroll, 0xFFC6C6C6); - } - stack.popPose(); - buffers.endBatch(); - stack.popPose(); - - stack.pushPose(); - stack.translate(bx, by, 0); - for(int i = 0; i < maxDisplay; i++) { - int idx = begin + i; - T item = items.get(idx); - stack.pushPose(); - if(idx == selected) { - RenderHelper.fillRect(stack.last().pose(), iw, rh, 0x40000000); - } - renderer.render(stack, item); - stack.popPose(); - stack.translate(0, rh, 0); - } - stack.popPose(); - } - - @Override - public boolean mouseScrolled(double x, double y, double d) { - if(!isMouseOver(x, y)) return false; - begin = Helpers.clamp(0, (int) (begin - d), maxBegin); - return true; - } - - @Override - public boolean mouseClicked(double x, double y, int b) { - if(!isMouseOver(x, y)) return false; - int slot = (int) ((y - by) / rh); - if(slot > maxDisplay - 1) return false; - selected = begin + slot; - responder.accept(items.get(selected)); - return true; - } - - @Override - public boolean isMouseOver(double x, double y) { - return x >= bx && x <= bx + iw && y >= by && y <= by + ih; - } - - @Override - public NarrationPriority narrationPriority() { - return NarrationPriority.NONE; - } - - @Override - public void updateNarration(NarrationElementOutput p_169152_) {} - - @FunctionalInterface - public interface EntryRenderer { - void render(PoseStack stack, T item); - } -} diff --git a/src/main/java/com/eerussianguy/blazemap/profiling/KnownMods.java b/src/main/java/com/eerussianguy/blazemap/integration/KnownMods.java similarity index 68% rename from src/main/java/com/eerussianguy/blazemap/profiling/KnownMods.java rename to src/main/java/com/eerussianguy/blazemap/integration/KnownMods.java index 8e6b9bac..bf3ea794 100644 --- a/src/main/java/com/eerussianguy/blazemap/profiling/KnownMods.java +++ b/src/main/java/com/eerussianguy/blazemap/integration/KnownMods.java @@ -1,29 +1,33 @@ -package com.eerussianguy.blazemap.profiling; +package com.eerussianguy.blazemap.integration; import java.lang.reflect.Array; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.function.Function; import java.util.stream.Collectors; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.ModContainer; import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; import com.eerussianguy.blazemap.BlazeMap; -import com.eerussianguy.blazemap.api.BlazeMapAPI; import com.eerussianguy.blazemap.api.BlazeRegistry; import com.eerussianguy.blazemap.api.debug.ModAnnouncementEvent; -import com.eerussianguy.blazemap.feature.ModIntegration.ModIDs; public class KnownMods { private static final HashMap CORE = new HashMap<>(); private static final HashMap COMPAT = new HashMap<>(); private static final HashMap PROBLEM = new HashMap<>(); - private static final HashMap APICALL = new HashMap<>(); + private static final HashMap API_CALL = new HashMap<>(); private static final HashMap ANNOUNCED = new HashMap<>(); private static final HashSet ALL_KNOWN = new HashSet<>(); + public static final String UNKNOWN_VERSION = "0.0NONE"; + public static final String DEVELOPMENT_VERSION = "(dev)"; + public static void init() { add(CORE, ModIDs.MINECRAFT); add(CORE, ModIDs.FORGE); @@ -36,25 +40,17 @@ public static void init() { add(PROBLEM, ModIDs.OPTIFINE); add(PROBLEM, ModIDs.CHUNKPREGEN); - add(BlazeMapAPI.MASTER_DATA); - add(BlazeMapAPI.COLLECTORS); - add(BlazeMapAPI.TRANSFORMERS); - add(BlazeMapAPI.PROCESSORS); - add(BlazeMapAPI.LAYERS); - add(BlazeMapAPI.MAPTYPES); - add(BlazeMapAPI.OBJECT_RENDERERS); - HashSet mods = new HashSet<>(); MinecraftForge.EVENT_BUS.post(new ModAnnouncementEvent(mods)); + + for(var integration : BlazeMap.INTEGRATIONS) { + if(!integration.enabled()) continue; + mods.addAll(Arrays.asList(integration.dependencies)); + } + for(String mod : mods){ add(ANNOUNCED, mod); } - - ALL_KNOWN.addAll(CORE.keySet()); - ALL_KNOWN.addAll(COMPAT.keySet()); - ALL_KNOWN.addAll(PROBLEM.keySet()); - ALL_KNOWN.addAll(APICALL.keySet()); - ALL_KNOWN.addAll(ANNOUNCED.keySet()); } @SafeVarargs @@ -69,6 +65,18 @@ public static boolean isAnyLoaded(Iterable ... lists) { return false; } + public static boolean isLoaded(String modID) { + return ModList.get().getModContainerById(modID).isPresent(); + } + + public static String getOwnerName(BlazeRegistry.Key key) { + return API_CALL.get(key.location.getNamespace()).name; + } + + public static String getOwnerName(ResourceLocation key) { + return API_CALL.get(key.getNamespace()).name; + } + @SafeVarargs public static T[] getCore(Class t, Function function, T ... fallbacks){ return mapEntries(CORE, t, function, fallbacks); @@ -86,7 +94,7 @@ public static T[] getProblem(Class t, Function func @SafeVarargs public static T[] getAPICall(Class t, Function function, T ... fallbacks){ - return mapEntries(APICALL, t, function, fallbacks); + return mapEntries(API_CALL, t, function, fallbacks); } @SafeVarargs @@ -103,16 +111,22 @@ private static T[] mapEntries(HashMap map, Class t, Func } } - private static void add(BlazeRegistry registry){ + public static void addIntegration(ModIntegration integration) { + add(COMPAT, integration.modID); + } + + public static void addRegistry(BlazeRegistry registry){ for(var key : registry.keys()){ String mod = key.location.getNamespace(); - APICALL.computeIfAbsent(mod, id -> new ModInfo(ModList.get().getModContainerById(id).get())); + add(API_CALL, mod); } } private static void add(HashMap map, String modId){ var container = ModList.get().getModContainerById(modId); - container.ifPresent(mod -> map.put(modId, new ModInfo(mod))); + if(container.isEmpty()) return; + map.put(modId, new ModInfo(container.get())); + ALL_KNOWN.add(modId); } public static class ModInfo { @@ -122,7 +136,14 @@ public ModInfo(ModContainer container){ this.id = container.getModId(); var info = container.getModInfo(); this.name = info.getDisplayName(); - this.version = info.getVersion().toString(); + this.version = coalesce(info.getVersion().toString()); + } + + private static String coalesce(String version) { + if(!FMLEnvironment.production && version.equals(UNKNOWN_VERSION)) { + return DEVELOPMENT_VERSION; + } + return version; } } } diff --git a/src/main/java/com/eerussianguy/blazemap/integration/ModIDs.java b/src/main/java/com/eerussianguy/blazemap/integration/ModIDs.java new file mode 100644 index 00000000..e4ed3311 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/integration/ModIDs.java @@ -0,0 +1,27 @@ +package com.eerussianguy.blazemap.integration; + +import java.util.List; + +public class ModIDs { + // Core + public static final String MINECRAFT = "minecraft"; + public static final String FORGE = "forge"; + + + // Compat + public static final String SODIUM = "sodium"; + public static final String EMBEDDIUM = "embeddium"; + public static final String RUBIDIUM = "rubidium"; + + public static final String FTB_CHUNKS = "ftbchunks"; + public static final String FTB_TEAMS = "ftbteams"; + public static final String FTB_LIBRARY = "ftblibrary"; + public static final String ARCHITECTURY = "architectury"; + + public static final List SODIUM_FAMILY = List.of(SODIUM, EMBEDDIUM, RUBIDIUM); + + + // Troublemakers + public static final String OPTIFINE = "optifine"; + public static final String CHUNKPREGEN = "chunkpregen"; +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/integration/ModIntegration.java b/src/main/java/com/eerussianguy/blazemap/integration/ModIntegration.java new file mode 100644 index 00000000..62b071b4 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/integration/ModIntegration.java @@ -0,0 +1,24 @@ +package com.eerussianguy.blazemap.integration; + +public abstract class ModIntegration { + public final String modID; + final String[] dependencies; + + public ModIntegration(String modID, String ... dependencies) { + this.modID = modID; + this.dependencies = dependencies; + } + + public boolean enabled() { + return KnownMods.isLoaded(modID); + } + + public final void init() { + if(enabled()) { + KnownMods.addIntegration(this); + setup(); + } + } + + public abstract void setup(); +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/integration/ftbchunks/FTBChunksOverlay.java b/src/main/java/com/eerussianguy/blazemap/integration/ftbchunks/FTBChunksOverlay.java new file mode 100644 index 00000000..67fbfb2a --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/integration/ftbchunks/FTBChunksOverlay.java @@ -0,0 +1,174 @@ +package com.eerussianguy.blazemap.integration.ftbchunks; + +import java.util.EnumMap; +import java.util.HashMap; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.maps.Overlay; +import com.eerussianguy.blazemap.api.maps.PixelSource; +import com.eerussianguy.blazemap.api.maps.TileResolution; +import com.eerussianguy.blazemap.api.util.RegionPos; +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.Helpers; +import dev.ftb.mods.ftbchunks.data.ClaimedChunkManager; +import dev.ftb.mods.ftbchunks.data.FTBChunksAPI; +import dev.ftb.mods.ftblibrary.math.ChunkDimPos; +import dev.ftb.mods.ftbteams.data.Team; + +public class FTBChunksOverlay extends Overlay { + private final EnumMap, FTBChunksPixelSource>> SOURCES = new EnumMap<>(TileResolution.class); + + public FTBChunksOverlay() { + super( + BlazeMapReferences.Overlays.FTBCHUNKS, + Helpers.translate("blazemap.ftbchunks"), + BlazeMap.resource("textures/map_icons/overlay_ftbchunks.png") + ); + } + + @Override + public PixelSource getPixelSource(ResourceKey dimension, RegionPos region, TileResolution resolution) { + return SOURCES + .computeIfAbsent(resolution, $ -> new HashMap<>()) + .computeIfAbsent(dimension, $ -> new FTBChunksPixelSource(dimension, resolution)) + .at(region); + } + + private static class FTBChunksPixelSource implements PixelSource { + private static final int REGION_CHUNKS = 32; // chunks in a region + private static final int CHUNK_OFFSET = 1; // depth of chunks from neighboring regions to lookup for borders + private static final int CHUNK_TOTAL = REGION_CHUNKS + CHUNK_OFFSET*2; // total width of chunk matrix (34 x 34) + private static final byte ALPHA_CENTER = (byte) 0x7F; + private static final byte ALPHA_BORDER = (byte) 0xFF; + + // Here be binary flags. These represent what edges of the chunk the current pixel touches. + private static final byte NO_EDGE = 0b0000; + private static final byte EDGE_X0 = 0b0001; + private static final byte EDGE_X1 = 0b0010; + private static final byte EDGE_Z0 = 0b0100; + private static final byte EDGE_Z1 = 0b1000; + private static final byte EDGE_ALL = 0b1111; + + // fast access shortcut to check neighbor chunks for claims based on pixel edge binary flags + private static final int[][][] EDGE_CHECKS = new int[16][][]; + static { + int[] x0 = {-1, 0}, x1 = { 1, 0}, z0 = { 0,-1}, z1 = { 0, 1}; // face coordinades + int[] c00 = {-1,-1}, c01 = {-1, 1}, c10 = { 1,-1}, c11 = { 1, 1}; // corner coordinates + + // non-edge pixels + EDGE_CHECKS[NO_EDGE] = new int[][]{}; + + // face pixels + EDGE_CHECKS[EDGE_X0] = new int[][]{ x0 }; + EDGE_CHECKS[EDGE_X1] = new int[][]{ x1 }; + EDGE_CHECKS[EDGE_Z0] = new int[][]{ z0 }; + EDGE_CHECKS[EDGE_Z1] = new int[][]{ z1 }; + + // corner pixels + EDGE_CHECKS[EDGE_X0 | EDGE_Z0] = new int[][]{ x0, z0, c00 }; + EDGE_CHECKS[EDGE_X0 | EDGE_Z1] = new int[][]{ x0, z1, c01 }; + EDGE_CHECKS[EDGE_X1 | EDGE_Z0] = new int[][]{ x1, z0, c10 }; + EDGE_CHECKS[EDGE_X1 | EDGE_Z1] = new int[][]{ x1, z1, c11 }; + + // extreme case, at 1:16 entire chunk would be a single pixel, touching every edge + EDGE_CHECKS[EDGE_ALL] = new int[][]{x0, x1, z0, z1, c00, c01, c10, c11}; + } + + private final Team[][] claims = new Team[CHUNK_TOTAL][CHUNK_TOTAL]; + private final ResourceKey dimension; + private final TileResolution resolution; + + private FTBChunksPixelSource(ResourceKey dimension, TileResolution resolution) { + this.resolution = resolution; + this.dimension = dimension; + } + + /** Set this source to a specific region. Causes it to cache all claims in advance. */ + private FTBChunksPixelSource at(RegionPos region) { + int beginX = region.x << 5; + int beginZ = region.z << 5; + + var manager = FTBChunksAPI.getManager(); + for(int x = -CHUNK_OFFSET; x < REGION_CHUNKS + CHUNK_OFFSET; x++) { + for(int z = -CHUNK_OFFSET; z < REGION_CHUNKS + CHUNK_OFFSET; z++) { + claims[x + CHUNK_OFFSET][z + CHUNK_OFFSET] = getOwner(manager, beginX + x, beginZ + z); + } + } + + return this; + } + + private Team getOwner(ClaimedChunkManager manager, int x, int z) { + var claim = manager.getChunk(new ChunkDimPos(dimension, x, z)); + if(claim == null) return null; + return claim.getTeamData().getTeam(); + } + + @Override + public int getPixel(int x, int z) { + int chunkX = x / resolution.chunkWidth; + int chunkZ = z / resolution.chunkWidth; + Team team = claims[chunkX + CHUNK_OFFSET][chunkZ + CHUNK_OFFSET]; + + // Unclaimed, no need to do all the math. + if(team == null) return 0; + + // Get a 4-bit pack of flags for the edges this pixel touches. + int edgeFlags = getPixelEdges(x, z); + + // Given the edges our pixel touches, get a set of neighbor coordinates to test. + // If all checked neighbors are claimed by the same team, this pixel is inside the claim, + // otherwise it is in the edge of the claim and different opacity must be used. + var checks = EDGE_CHECKS[edgeFlags]; + + boolean isInside = true; + for(var coords : checks) { + Team neighbor = claims[chunkX + CHUNK_OFFSET + coords[0]][chunkZ + CHUNK_OFFSET + coords[1]]; + if(!team.equals(neighbor)) { + isInside = false; + break; + } + } + + int color = Colors.abgr(team.getColor()); + return Colors.setAlpha(color, isInside ? ALPHA_CENTER : ALPHA_BORDER); + } + + /** + * Gets a 4-bit pack of flags representing the edges of the chunk this pixel touches. + * Only 10 of the 16 possible values are actually legal, as follows: + * - No edges (pixel is in the center of the chunk) + * - One edge (x4) + * - Two edges (corner pixel) (x4) + * - All edges (scale is 1:16 and the pixel covers the entire chunk + * No illegal values can be returned by this function. + */ + private byte getPixelEdges(int x, int z) { + // The algorithm after this would produce the same result, this just saves doing the math. + // There actually isn't a TileResolution of 1:16 YET (at the time of this writing), but future proofing. + // There is no need to prepare for 1:32 or above because 1:16 is the current hard limit of what the engine can do. + if(resolution.chunkWidth == 1) return EDGE_ALL; + + int blockX = x % resolution.chunkWidth; + int blockZ = z % resolution.chunkWidth; + boolean x0 = blockX == 0, x1 = blockX == resolution.chunkWidth - 1; + boolean z0 = blockZ == 0, z1 = blockZ == resolution.chunkWidth - 1; + + return (byte) ((x0 ? EDGE_X0 : NO_EDGE) | (x1 ? EDGE_X1 : NO_EDGE) | (z0 ? EDGE_Z0 : NO_EDGE) | (z1 ? EDGE_Z1 : NO_EDGE)); + } + + @Override + public int getWidth() { + return resolution.regionWidth; + } + + @Override + public int getHeight() { + return resolution.regionWidth; + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/integration/ftbchunks/FTBChunksPlugin.java b/src/main/java/com/eerussianguy/blazemap/integration/ftbchunks/FTBChunksPlugin.java new file mode 100644 index 00000000..c788566e --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/integration/ftbchunks/FTBChunksPlugin.java @@ -0,0 +1,68 @@ +package com.eerussianguy.blazemap.integration.ftbchunks; + +import java.util.Set; + +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.common.MinecraftForge; + +import com.eerussianguy.blazemap.BlazeMap; +import com.eerussianguy.blazemap.api.BlazeMapReferences; +import com.eerussianguy.blazemap.api.event.BlazeRegistryEvent.OverlayRegistryEvent; +import com.eerussianguy.blazemap.api.event.ComponentOrderingEvent.OverlayOrderingEvent; +import com.eerussianguy.blazemap.api.event.MapMenuSetupEvent; +import com.eerussianguy.blazemap.integration.ModIDs; +import com.eerussianguy.blazemap.integration.ModIntegration; +import com.eerussianguy.blazemap.lib.Helpers; +import dev.ftb.mods.ftbchunks.data.ClaimedChunk; +import dev.ftb.mods.ftbchunks.data.FTBChunksAPI; +import dev.ftb.mods.ftblibrary.math.ChunkDimPos; + +public class FTBChunksPlugin extends ModIntegration { + private static final ResourceLocation CLAIM_ID = BlazeMap.resource("map.menu.ftbchunks.claim"); + private static final ResourceLocation UNCLAIM_ID = BlazeMap.resource("map.menu.ftbchunks.unclaim"); + private static final ResourceLocation CLAIMED_ID = BlazeMap.resource("map.menu.ftbchunks.claimed"); + private static final TranslatableComponent CLAIM_TEXT = Helpers.translate("blazemap.gui.worldmap.menu.ftbchunks.claim"); + private static final TranslatableComponent UNCLAIM_TEXT = Helpers.translate("blazemap.gui.worldmap.menu.ftbchunks.unclaim"); + private static final TranslatableComponent CLAIMED_TEXT = Helpers.translate("blazemap.gui.worldmap.menu.ftbchunks.claimed"); + private static final MapMenuSetupEvent.MenuAction CLAIMED = new MapMenuSetupEvent.MenuAction(CLAIMED_ID, null, CLAIMED_TEXT, null); + + public FTBChunksPlugin() { + super( + ModIDs.FTB_CHUNKS, + ModIDs.FTB_TEAMS, ModIDs.FTB_LIBRARY, ModIDs.ARCHITECTURY + ); + } + + @Override + public void setup() { + MinecraftForge.EVENT_BUS.addListener((OverlayRegistryEvent event) -> { + event.registry.register(new FTBChunksOverlay()); + }); + + MinecraftForge.EVENT_BUS.addListener((OverlayOrderingEvent event) -> { + event.addAfter(BlazeMapReferences.Overlays.FTBCHUNKS, Set.of(BlazeMapReferences.Overlays.GRID)); + }); + + MinecraftForge.EVENT_BUS.addListener((MapMenuSetupEvent event) -> { + if(!event.overlays.contains(BlazeMapReferences.Overlays.FTBCHUNKS)) return; + + final var uuid = Helpers.getPlayer().getUUID(); + final var manager = FTBChunksAPI.getManager(); + final var pos = new ChunkDimPos(event.dimension, event.chunkPosX, event.chunkPosZ); + final var claim = manager.getChunk(pos); + + if(claim == null) { + event.root.add(new MapMenuSetupEvent.MenuAction(CLAIM_ID, null, CLAIM_TEXT, () -> { + manager.registerClaim(pos, new ClaimedChunk(manager.getPersonalData(uuid), pos)); + })); + } else if(claim.getTeamData().isTeamMember(uuid)) { + event.root.add(new MapMenuSetupEvent.MenuAction(UNCLAIM_ID, null, UNCLAIM_TEXT, () -> { + manager.unregisterClaim(pos); + })); + } else { + event.root.add(CLAIMED); + } + }); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/ArraySet.java b/src/main/java/com/eerussianguy/blazemap/lib/ArraySet.java new file mode 100644 index 00000000..a0523a2a --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/ArraySet.java @@ -0,0 +1,128 @@ +package com.eerussianguy.blazemap.lib; + +import java.util.*; + +import org.jetbrains.annotations.NotNull; + +/** + * This is obvious, don't make me explain myself. But TL;DR: + * + * Java has all these complicated Set implementations and yet it doesn't offer a single simple one. + * This is backed by an array, orderable, etc. Basically just an ArrayList that enforces uniqueness. + * The intent here is to achieve high performance on tiny sets that don't get changed too much. + * This is NOT meant for general use. + * + * @author LordFokas + */ +public class ArraySet implements Set { + private final ArrayList list; + + @SafeVarargs + public static ArraySet of(E... elements) { + Objects.requireNonNull(elements); + ArraySet set = new ArraySet<>(elements.length); + for(E element : elements) { + set.add(element); + } + return set; + } + + public ArraySet() { + list = new ArrayList<>(); + } + + public ArraySet(int initialCapacity) { + list = new ArrayList<>(initialCapacity); + } + + public ArraySet(Collection initialValues) { + this(initialValues.size()); + addAll(initialValues); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object element) { + return list.contains(element); + } + + @NotNull + @Override + public Iterator iterator() { + return list.iterator(); + } + + @NotNull + @Override + public Object[] toArray() { + return list.toArray(); + } + + @NotNull + @Override + public T[] toArray(@NotNull T[] array) { + return list.toArray(array); + } + + @Override + public boolean add(E element) { + if(list.contains(element)) return false; + return list.add(element); + } + + /** ArrayList functionality */ + public boolean add(int index, E element) { + if(list.contains(element)) return false; + list.add(index, element); + return true; + } + + /** ArrayList functionality */ + public int indexOf(E element) { + return list.indexOf(element); + } + + /** ArrayList functionality */ + public E get(int index) { + return list.get(index); + } + + @Override + public boolean remove(Object element) { + return list.remove(element); + } + + @Override + public boolean containsAll(@NotNull Collection collection) { + return list.containsAll(collection); + } + + @Override + public boolean addAll(Collection collection) { + return list.addAll(collection.stream().distinct().filter(e -> !contains(e)).toList()); + } + + @Override + public boolean retainAll(@NotNull Collection collection) { + return list.retainAll(collection); + } + + @Override + public boolean removeAll(@NotNull Collection collection) { + return list.removeAll(collection); + } + + @Override + public void clear() { + list.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/lib/Colors.java b/src/main/java/com/eerussianguy/blazemap/lib/Colors.java new file mode 100644 index 00000000..160f81ae --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/Colors.java @@ -0,0 +1,175 @@ +package com.eerussianguy.blazemap.lib; + +import java.awt.*; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class Colors { + public static final int ALPHA = 0xFF000000; + public static final int NO_TINT = -1; + public static final int WHITE = 0xFFFFFFFF; + public static final int BLACK = ALPHA; + public static final int DISABLED = 0xFF666666; + public static final int UNFOCUSED = 0xFFA0A0A0; + public static final int LABEL_COLOR = 0xFF404040; + public static final int WIDGET_BACKGROUND = 0xA0000000; + + protected static final Map darknessPointCache = new ConcurrentHashMap(); + + public static int layerBlend(int bottom, int top) { + if((top & ALPHA) == ALPHA) return top; // top is opaque, use top + if((top & ALPHA) == 0) return bottom; // top is transparent, use bottom + if((bottom & ALPHA) == 0) return top; // bottom is transparent, use top + + float point = ((float) (top >> 24)) / 255F; + return 0xFF000000 | interpolate(bottom, 0, top, 1, point); + } + + public static int interpolate(int color1, float key1, int color2, float key2, float point) { + point = (point - key1) / (key2 - key1); + int b0 = interpolate((color1 >> 24) & 0xFF, (color2 >> 24) & 0xFF, point); + int b1 = interpolate((color1 >> 16) & 0xFF, (color2 >> 16) & 0xFF, point); + int b2 = interpolate((color1 >> 8) & 0xFF, (color2 >> 8) & 0xFF, point); + int b3 = interpolate(color1 & 0xFF, color2 & 0xFF, point); + return b0 << 24 | b1 << 16 | b2 << 8 | b3; + } + + public static int interpolate(int a, int b, float p) { + a *= (1F - p); + b *= p; + return Math.max(0, Math.min(255, a + b)); + } + + public static int multiplyRGB(int a, int b) { + int _r = multiplyChannel((a >> 16) & 0xFF, (b >> 16) & 0xFF); + int _g = multiplyChannel((a >> 8) & 0xFF, (b >> 8) & 0xFF); + int _b = multiplyChannel(a & 0xFF, b & 0xFF); + return (_r << 16) | (_g << 8) | _b; + } + + public static int multiplyChannel(float a, float b) { + return 0xFF & (int) (a * b / 255F); + } + + public static int setAlpha(int color, byte alpha) { + return (alpha << 24) | (color & 0x00FFFFFF); + } + + public static float interpolate(float a, float b, float p) { + a *= (1F - p); + b *= p; + return Math.max(0, Math.min(1.0f, a + b)); + } + + + /** + * This is a value to represent the lessening light as it attenuates while passing through semitransparent objects. + * Will always be in the range [0 - 0.96667] (aka 0 to almost but not quite 1). + * Higher is more shadowed. + * + * @param depth How many transparent blocks the light has passed through + */ + public static float getDarknessPoint(int depth) { + return darknessPointCache.computeIfAbsent(depth, (size) -> { + return Math.min(2.90f, 0.375f * (float)Math.log(size + 1)) / 3; + }); + } + + + /** + * Calculate the colour of the bottom block as seen through the top block + * + * @param top The higher block colour values + * @param bottom The lower block colour values + * @param depth How many transparent blocks are above this one + * @return + */ + public static float[] filterARGB(float[] top, float[] bottom, int depth) { + // Adjust bottom brightness for light attenuation + float point = getDarknessPoint(depth); + + for (int i = 1; i < 4; i++) { + // Attenuate from depth + bottom[i] -= bottom[i] * (point); + + // Filter the below colour through the above colour + bottom[i] = interpolate(bottom[i], top[i], top[0]); + } + + // Adjust opacity + bottom[0] = 1 - ((1 - bottom[0]) * (1 - top[0])); + + return bottom; + } + + public static int abgr(Color color) { + return color.getAlpha() << 24 | color.getBlue() << 16 | color.getGreen() << 8 | color.getRed(); + } + + public static int abgr(int color) { + int r = color & 0xFF0000; + int b = color & 0x0000FF; + color &= 0xFF00FF00; + return color | (b << 16) | (r >> 16); + } + + public static int HSB2RGB(float[] hsb) { + return Color.HSBtoRGB(hsb[0], hsb[1], hsb[2]); + } + + public static int HSB2RGB(float h, float s, float b) { + return Color.HSBtoRGB(h, s, b); + } + + public static float[] RGB2HSB(int color) { + float[] hsb = new float[3]; + int r = (color & 0xFF0000) >> 16; + int g = (color & 0xFF00) >> 8; + int b = color & 0xFF; + Color.RGBtoHSB(r, g, b, hsb); + return hsb; + } + + public static int randomBrightColor() { + float hue = ((float) (System.nanoTime() / 1000) % 360) / 360F; + return Color.HSBtoRGB(hue, 1, 1); + } + + public static float[] decomposeRGBA(int color) { + float a = ((color >> 24) & 0xFF) / 255f; + float r = ((color >> 16) & 0xFF) / 255f; + float g = ((color >> 8) & 0xFF) / 255f; + float b = ((color) & 0xFF) / 255f; + return new float[] {a, r, g, b}; + } + public static float[] decomposeRGBA(int color, float[] arr) { + arr[0] = ((color >> 24) & 0xFF) / 255f; + arr[1] = ((color >> 16) & 0xFF) / 255f; + arr[2] = ((color >> 8) & 0xFF) / 255f; + arr[3] = ((color) & 0xFF) / 255f; + return arr; + } + + public static int recomposeRGBA(float[] color) { + int a = (((int)(color[0] * 255f)) & 0xFF); + int r = (((int)(color[1] * 255f)) & 0xFF); + int g = (((int)(color[2] * 255f)) & 0xFF); + int b = (((int)(color[3] * 255f)) & 0xFF); + return a << 24 | r << 16 | g << 8 | b; + } + + public static float[] decomposeRGB(int color) { + float r = ((color >> 16) & 0xFF) / 255f; + float g = ((color >> 8) & 0xFF) / 255f; + float b = ((color) & 0xFF) / 255f; + return new float[] {r, g, b}; + } + + public static int[] decomposeIntRGBA(int color) { + int a = (int)((color >> 24) & 0xFF); + int r = (int)((color >> 16) & 0xFF); + int g = (int)((color >> 8) & 0xFF); + int b = (int)((color) & 0xFF); + return new int[] {a, r, g, b}; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/util/Helpers.java b/src/main/java/com/eerussianguy/blazemap/lib/Helpers.java similarity index 62% rename from src/main/java/com/eerussianguy/blazemap/util/Helpers.java rename to src/main/java/com/eerussianguy/blazemap/lib/Helpers.java index 784d0896..98513790 100644 --- a/src/main/java/com/eerussianguy/blazemap/util/Helpers.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/Helpers.java @@ -1,33 +1,19 @@ -package com.eerussianguy.blazemap.util; +package com.eerussianguy.blazemap.lib; -import java.io.File; +import java.util.Calendar; import java.util.Objects; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.player.LocalPlayer; import net.minecraft.core.BlockPos; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtOps; import net.minecraft.network.chat.TranslatableComponent; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.Entity; -import net.minecraft.world.level.storage.LevelResource; -import com.eerussianguy.blazemap.BlazeMap; import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.serialization.Codec; import org.jetbrains.annotations.Nullable; -import static com.eerussianguy.blazemap.BlazeMap.MOD_ID; - public class Helpers { - private static File baseDir; - - public static ResourceLocation identifier(String name) { - return new ResourceLocation(MOD_ID, name); - } - public static ClientLevel levelOrThrow() { return Objects.requireNonNull(Minecraft.getInstance().level); } @@ -65,32 +51,10 @@ public static boolean isIntegratedServerRunning() { return Minecraft.getInstance().hasSingleplayerServer(); } - /** - * @return the client-side dir to store data for the currently connected server - */ - public static File getClientSideStorageDir() { - Minecraft mc = Minecraft.getInstance(); - if(mc.hasSingleplayerServer()) { - return new File(mc.getSingleplayerServer().getWorldPath(LevelResource.ROOT).toFile(), "blazemap-client"); - } - else { - if(baseDir == null) baseDir = new File(mc.gameDirectory, "blazemap-servers"); - return new File(baseDir, mc.getCurrentServer().ip.replace(':', '+')); - } - } - public static void runOnMainThread(Runnable r) { Minecraft.getInstance().tell(r); } - public static void writeCodec(Codec codec, T value, CompoundTag tag, String field) { - tag.put(field, codec.encodeStart(NbtOps.INSTANCE, value).getOrThrow(false, BlazeMap.LOGGER::error)); - } - - public static T decodeCodec(Codec codec, CompoundTag tag, String field) { - return codec.parse(NbtOps.INSTANCE, tag.get(field)).getOrThrow(false, BlazeMap.LOGGER::error); - } - public static TranslatableComponent translate(String key) { return new TranslatableComponent(key); } @@ -103,6 +67,10 @@ public static int clamp(int min, int var, int max) { return Math.max(min, Math.min(var, max)); } + public static float clamp(float min, float var, float max) { + return Math.max(min, Math.min(var, max)); + } + public static double clamp(double min, double var, double max) { return Math.max(min, Math.min(var, max)); } @@ -113,4 +81,22 @@ public static void closeQuietly(AutoCloseable closeable) { catch(Exception ignored) {} } } + + public static String getISO8601(char d, char t, char h) { + Calendar calendar = Calendar.getInstance(); + int year = calendar.get(Calendar.YEAR); + int month = calendar.get(Calendar.MONTH) + 1; // fuck you too Java + int day = calendar.get(Calendar.DAY_OF_MONTH); + int hour = calendar.get(Calendar.HOUR_OF_DAY); + int minute = calendar.get(Calendar.MINUTE); + int second = calendar.get(Calendar.SECOND); + return String.format("%04d%s%02d%s%02d%s%02d%s%02d%s%02d", year, d, month, d, day, t, hour, h, minute, h, second); + } + + @SuppressWarnings("unchecked") + public static > T cycle(T current, int direction) { + T[] values = (T[]) current.getClass().getEnumConstants(); + int index = (current.ordinal() + values.length + direction) % values.length; + return values[index]; + } } diff --git a/src/main/java/com/eerussianguy/blazemap/lib/InheritedBoolean.java b/src/main/java/com/eerussianguy/blazemap/lib/InheritedBoolean.java new file mode 100644 index 00000000..c60040af --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/InheritedBoolean.java @@ -0,0 +1,31 @@ +package com.eerussianguy.blazemap.lib; + +import java.util.function.BooleanSupplier; + +public enum InheritedBoolean { + TRUE, FALSE, DEFAULT; + + public static InheritedBoolean of(boolean value) { + return value ? TRUE : FALSE; + } + + public boolean getOrInherit(BooleanSupplier parent) { + return switch(this) { + case TRUE -> true; + case FALSE -> false; + case DEFAULT -> parent.getAsBoolean(); + }; + } + + public boolean getOrThrow() { + return switch(this) { + case TRUE -> true; + case FALSE -> false; + case DEFAULT -> throw new IllegalStateException("DEFAULT has no direct value"); + }; + } + + public boolean isDirect() { + return this != DEFAULT; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/util/IntHolder.java b/src/main/java/com/eerussianguy/blazemap/lib/IntHolder.java similarity index 91% rename from src/main/java/com/eerussianguy/blazemap/util/IntHolder.java rename to src/main/java/com/eerussianguy/blazemap/lib/IntHolder.java index a33e6f02..3684962a 100644 --- a/src/main/java/com/eerussianguy/blazemap/util/IntHolder.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/IntHolder.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.util; +package com.eerussianguy.blazemap.lib; public class IntHolder { private int number; diff --git a/src/main/java/com/eerussianguy/blazemap/lib/ObjHolder.java b/src/main/java/com/eerussianguy/blazemap/lib/ObjHolder.java new file mode 100644 index 00000000..282f1186 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/ObjHolder.java @@ -0,0 +1,35 @@ +package com.eerussianguy.blazemap.lib; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class ObjHolder { + private T value; + private Consumer responder = $ -> {}; + + public ObjHolder() { + this(null); + } + + public ObjHolder(T value) { + this.value = value; + } + + public T get() { + return value; + } + + public void set(T value) { + this.value = value; + responder.accept(value); + } + + public void mutate(Function transform) { + set(transform.apply(value)); + } + + public void setResponder(Consumer responder) { + this.responder = responder; + responder.accept(value); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/RegistryHelper.java b/src/main/java/com/eerussianguy/blazemap/lib/RegistryHelper.java new file mode 100644 index 00000000..ac477784 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/RegistryHelper.java @@ -0,0 +1,36 @@ +package com.eerussianguy.blazemap.lib; + +import java.util.HashMap; +import java.util.Set; + +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +public class RegistryHelper { + private static final HashMap> DIMENSION_MAP = new HashMap<>(); + private static final ArraySet> DIMENSION_SET = new ArraySet<>(); + + static { + addDimension(Level.OVERWORLD); + addDimension(Level.NETHER); + addDimension(Level.END); + } + + private static void addDimension(ResourceKey dimension) { + DIMENSION_MAP.put(dimension.location(), dimension); + DIMENSION_SET.add(dimension); + } + + public static ResourceKey getDimension(ResourceLocation dimension) { + return DIMENSION_MAP.computeIfAbsent(dimension, d -> ResourceKey.create(Registry.DIMENSION_REGISTRY, d)); + } + + public static Set> getAllDimensions() { + if(DIMENSION_SET.size() < DIMENSION_MAP.size()) { + DIMENSION_SET.addAll(DIMENSION_MAP.values()); + } + return DIMENSION_SET; + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/RenderHelper.java b/src/main/java/com/eerussianguy/blazemap/lib/RenderHelper.java new file mode 100644 index 00000000..77939ed6 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/RenderHelper.java @@ -0,0 +1,157 @@ +package com.eerussianguy.blazemap.lib; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.BlazeMap; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import com.mojang.math.Matrix4f; + +public class RenderHelper { + private static final int[] CHROMA = { 0xFFFF0000, 0xFFFFFF00, 0xFF00FF00, 0xFF00FFFF, 0xFF0000FF, 0xFFFF00FF }; + public static final RenderType SOLID = RenderType.text(BlazeMap.resource("textures/solid.png")); // FIXME: lib class shouldn't have BlazeMap reference + public static final float F0 = 0, F1 = 1; + + public static void drawTexturedQuad(ResourceLocation texture, int color, PoseStack stack, int px, int py, int w, int h) { + drawTexturedQuad(texture, color, stack, px, px + w, py, py + h, 0, w, h, 0, 0, w, h); + } + + private static void drawTexturedQuad(ResourceLocation texture, int color, PoseStack stack, int px0, int px1, int py0, int py1, int pz, int wp, int hp, float tx0, float ty0, int tw, int th) { + drawTexturedQuad(texture, color, stack.last().pose(), px0, px1, py0, py1, pz, (tx0 + 0.0F) / (float)tw, (tx0 + (float)wp) / (float)tw, (ty0 + 0.0F) / (float)th, (ty0 + (float)hp) / (float)th); + } + + private static void drawTexturedQuad(ResourceLocation texture, int color, Matrix4f matrix, int px0, int px1, int py0, int py1, int pz, float u0, float u1, float v0, float v1) { + setShaderColor(color); + RenderSystem.setShaderTexture(0, texture); + RenderSystem.setShader(ShaderHelper::getTextureShader); + RenderSystem.enableBlend(); + BufferBuilder bufferbuilder = Tesselator.getInstance().getBuilder(); + bufferbuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + bufferbuilder.vertex(matrix, (float)px0, (float)py1, (float)pz).uv(u0, v1).endVertex(); + bufferbuilder.vertex(matrix, (float)px1, (float)py1, (float)pz).uv(u1, v1).endVertex(); + bufferbuilder.vertex(matrix, (float)px1, (float)py0, (float)pz).uv(u1, v0).endVertex(); + bufferbuilder.vertex(matrix, (float)px0, (float)py0, (float)pz).uv(u0, v0).endVertex(); + bufferbuilder.end(); + BufferUploader.end(bufferbuilder); + } + + public static void setShaderColor(int color) { + float a = ((float) ((color >> 24) & 0xFF)) / 255F; + float r = ((float) ((color >> 16) & 0xFF)) / 255F; + float g = ((float) ((color >> 8) & 0xFF)) / 255F; + float b = ((float) ((color) & 0xFF)) / 255F; + RenderSystem.setShaderColor(r, g, b, a); + } + + public static void drawQuad(VertexConsumer vertices, Matrix4f matrix, float w, float h) { + drawQuad(vertices, matrix, w, h, Colors.NO_TINT, 0F, 1F, 0F, 1F); + } + + public static void drawQuad(VertexConsumer vertices, Matrix4f matrix, float w, float h, int color) { + drawQuad(vertices, matrix, w, h, color, 0F, 1F, 0F, 1F); + } + + public static void drawQuad(VertexConsumer vertices, Matrix4f matrix, float w, float h, int color, float u0, float u1, float v0, float v1) { + float a = ((float) ((color >> 24) & 0xFF)) / 255F; + float r = ((float) ((color >> 16) & 0xFF)) / 255F; + float g = ((float) ((color >> 8) & 0xFF)) / 255F; + float b = ((float) ((color) & 0xFF)) / 255F; + vertices.vertex(matrix, 0.0F, h, -0.01F).color(r, g, b, a).uv(u0, v1).uv2(LightTexture.FULL_BRIGHT).endVertex(); + vertices.vertex(matrix, w, h, -0.01F).color(r, g, b, a).uv(u1, v1).uv2(LightTexture.FULL_BRIGHT).endVertex(); + vertices.vertex(matrix, w, 0.0F, -0.01F).color(r, g, b, a).uv(u1, v0).uv2(LightTexture.FULL_BRIGHT).endVertex(); + vertices.vertex(matrix, 0.0F, 0.0F, -0.01F).color(r, g, b, a).uv(u0, v0).uv2(LightTexture.FULL_BRIGHT).endVertex(); + } + + public static void renderChromaticGradient(PoseStack stack, float w, float h){ + var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + stack.pushPose(); + w /= 6; + VertexConsumer vertices = buffers.getBuffer(RenderHelper.SOLID); + for(int i = 0; i < 6; i++) { + int colorA = CHROMA[ i % 6 ], colorB = CHROMA[ (i+1) % 6 ]; + Matrix4f matrix = stack.last().pose(); + renderGradient(vertices, matrix, w, h, colorA, colorB, colorB, colorA); + stack.translate(w, 0, 0); + } + stack.popPose(); + buffers.endBatch(); + } + + // the 4 ints are the colors for each corner, in cardinal positions + public static void renderGradient(PoseStack stack, float w, float h, int nw, int ne, int se, int sw) { + var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + Matrix4f matrix = stack.last().pose(); + VertexConsumer vertices = buffers.getBuffer(RenderHelper.SOLID); + renderGradient(vertices, matrix, w, h, nw, ne, se, sw); + buffers.endBatch(); + } + + // the 4 ints are the colors for each corner, in cardinal positions + public static void renderGradient(VertexConsumer vertices, Matrix4f matrix, float w, float h, int nw, int ne, int se, int sw) { + vertices.vertex(matrix, F0, h , F0).color(sw).uv(F0, F1).uv2(LightTexture.FULL_BRIGHT).endVertex(); + vertices.vertex(matrix, w , h , F0).color(se).uv(F1, F1).uv2(LightTexture.FULL_BRIGHT).endVertex(); + vertices.vertex(matrix, w , F0, F0).color(ne).uv(F1, F0).uv2(LightTexture.FULL_BRIGHT).endVertex(); + vertices.vertex(matrix, F0, F0, F0).color(nw).uv(F0, F0).uv2(LightTexture.FULL_BRIGHT).endVertex(); + } + + public static void fillRect(MultiBufferSource buffers, Matrix4f matrix, float w, float h, int color) { + drawQuad(buffers.getBuffer(SOLID), matrix, w, h, color); + } + + public static void fillRect(Matrix4f matrix, float w, float h, int color) { + var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + drawQuad(buffers.getBuffer(SOLID), matrix, w, h, color); + buffers.endBatch(); + } + + public static void drawFrame(VertexConsumer vertices, PoseStack stack, int width, int height, int border) { + drawFrame(vertices, stack, width, height, border, Colors.NO_TINT); + } + + public static void drawFrame(VertexConsumer vertices, PoseStack stack, int width, int height, int border, int color) { + stack.pushPose(); + + drawQuad(vertices, stack.last().pose(), border, border, color, 0F, 0.25F, 0F, 0.25F); + stack.translate(border, 0, 0); + drawQuad(vertices, stack.last().pose(), width - (border * 2), border, color, 0.25F, 0.75F, 0F, 0.25F); + stack.translate(width - (border * 2), 0, 0); + drawQuad(vertices, stack.last().pose(), border, border, color, 0.75F, 1F, 0F, 0.25F); + + stack.translate(-width + border, border, 0); + + drawQuad(vertices, stack.last().pose(), border, height - (border * 2), color, 0F, 0.25F, 0.25F, 0.75F); + stack.translate(border, 0, 0); + drawQuad(vertices, stack.last().pose(), width - (border * 2), height - (border * 2), color, 0.25F, 0.75F, 0.25F, 0.75F); + stack.translate(width - (border * 2), 0, 0); + drawQuad(vertices, stack.last().pose(), border, height - (border * 2), color, 0.75F, 1F, 0.25F, 0.75F); + + stack.translate(-width + border, height - (border * 2), 0); + + drawQuad(vertices, stack.last().pose(), border, border, color, 0F, 0.25F, 0.75F, 1F); + stack.translate(border, 0, 0); + drawQuad(vertices, stack.last().pose(), width - (border * 2), border, color, 0.25F, 0.75F, 0.75F, 1F); + stack.translate(width - (border * 2), 0, 0); + drawQuad(vertices, stack.last().pose(), border, border, color, 0.75F, 1F, 0.75F, 1F); + + stack.popPose(); + } + + public static void renderWithScissorScaled(int x, int y, int w, int h, Runnable function) { + var window = Minecraft.getInstance().getWindow(); + double scale = (int) window.getGuiScale(); + RenderSystem.enableScissor((int)(x * scale), window.getHeight() - (int)((y+h) * scale), (int)(w * scale), (int)(h * scale)); + function.run(); + RenderSystem.disableScissor(); + } + + public static void renderWithScissorNative(int x, int y, int w, int h, Runnable function) { + var window = Minecraft.getInstance().getWindow(); + RenderSystem.enableScissor(x, window.getHeight() - (y+h), w, h); + function.run(); + RenderSystem.disableScissor(); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/ShaderHelper.java b/src/main/java/com/eerussianguy/blazemap/lib/ShaderHelper.java new file mode 100644 index 00000000..ab80fd88 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/ShaderHelper.java @@ -0,0 +1,45 @@ +package com.eerussianguy.blazemap.lib; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.ShaderInstance; +import net.minecraft.resources.ResourceLocation; + +import com.mojang.blaze3d.vertex.DefaultVertexFormat; + +public class ShaderHelper { + private static final HashMap shaders = new HashMap<>(); + private static final ArrayList textureShaderStack = new ArrayList<>(); + + public static ShaderInstance getTextureShader() { + if(textureShaderStack.size() > 0) { + return textureShaderStack.get(0); + } + return GameRenderer.getPositionTexShader(); + } + + public static void withTextureShader(ResourceLocation shader, Runnable function) { + textureShaderStack.add(0, getShader(shader)); + try { + function.run(); + } + finally { + textureShaderStack.remove(0); + } + } + + public static ShaderInstance getShader(ResourceLocation name) { + return shaders.computeIfAbsent(name, $ -> { + try { + return new ShaderInstance(Minecraft.getInstance().getResourceManager(), name, DefaultVertexFormat.POSITION_TEX); + } + catch(IOException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/Transparency.java b/src/main/java/com/eerussianguy/blazemap/lib/Transparency.java new file mode 100644 index 00000000..9ca4fc7a --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/Transparency.java @@ -0,0 +1,349 @@ +package com.eerussianguy.blazemap.lib; + +import java.util.Map; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.FluidTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.AbstractGlassBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.HalfTransparentBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.shapes.VoxelShape; + +/** + * Minecraft doesn't have an easy way to tell if a block will render with transparency or not + * based on the block data itself. There seems to be _no_ way to do this on the server side based + * on the information Minecraft gives us alone. Thus, it's up to us to determine which blocks + * should be considered "transparent" for mapping purposes. + * + * The following 6 sets allow us, plus other mods via API, to decide which blocks should be + * considered transparent by BM, and which should not. This can be determined based on a block's + * class (including superclasses), its block tags, or its fluid tags. Add the class or tag to the + * appropriate list for it to count. + * + * The default transparency is quite translucent, blocking most of what's underneath but still + * giving hints of what's below. This is for blocks like ice which aren't entirely see-through but + * still transmit some light. For the minority of blocks that are properly clear like stained glass, + * you'll want to also add them to the "quite transparent" lists. This will make them show a lot + * more of what's underneath including contour shadows. + * + * In practice, a block is expected to pass the `isTransparent` check if it's also expected to pass + * the `isQuiteTransparent` check. However, each check doesn't need to pass for the same reason. + * For example: Stained glass passes the "isTransparent" check because it subclasses `HalfTransparentBlock`. + * But it passes the "isQuiteTransparent" check because it subclasses `AbstractGlassBlock`, which + * is a subclass of `HalfTransparentBlock`. There is no reason to check it's a `HalfTransparentBlock` + * again if we've already checked that it's an `AbstractGlassBlock`. + */ +public class Transparency { + public static final float OPACITY_LOW = 0.1875f; // 3/16ths + public static final float OPACITY_HIGH = 0.875f; // 7/8ths + + private static final Set> transparentClasses = initialiseTransparentClassSet(false); + private static final Set> quiteTransparentClasses = initialiseTransparentClassSet(true); + + private static final Set> transparentFluidTags = initialiseTransparentFluidSet(false); + private static final Set> quiteTransparentFluidTags = initialiseTransparentFluidSet(true); + + private static final Set> transparentBlockTags = initialiseTransparentBlockSet(false); + private static final Set> quiteTransparentBlockTags = initialiseTransparentBlockSet(true); + + private static final Map knownBlocks = new ConcurrentHashMap<>(); + + public enum TransparencyState { + AIR(0f), + QUITE_TRANSPARENT(OPACITY_LOW), + SEMI_TRANSPARENT(OPACITY_HIGH), + OPAQUE(1f), + ; + + /** The value used for RGB calculations. + * 0 = fully see through, 1 = fully light blocking */ + public final float opacity; + /** The inverse of opacity (here only as syntactic sugar) */ + public final float transparency; + + private TransparencyState(float opacity) { + this.opacity = opacity; + this.transparency = 1 - opacity; + } + + public static TransparencyState max(TransparencyState t1, TransparencyState t2) { + if (t1.opacity > t2.opacity) return t1; + return t2; + } + + public static TransparencyState min(TransparencyState t1, TransparencyState t2) { + if (t1.opacity < t2.opacity) return t1; + return t2; + } + + public static boolean isAtLeastAsTransparentAs(TransparencyState t1, TransparencyState t2) { + return t1.opacity <= t2.opacity; + } + } + + public enum CompositionState { + BLOCK, // Eg: Stone, Slab (looks solid from the sky) + NON_FULL_BLOCK, // Eg: Torch, Iron Bar, Door + FLUIDLOGGED_BLOCK, // Eg: Waterlogged Enchanting Table + FLUIDLOGGED_NON_FULL, // Eg: Waterlogged Seagrass + FLUID, // Eg: Water, Lava + AIR, // Should only represent air blocks + } + + private static final Set> initialiseTransparentClassSet (boolean isQuiteTransparent) { + Set> newTransparencyList = new HashSet>(); + + if (isQuiteTransparent) { + newTransparencyList.add(AbstractGlassBlock.class); // Glass of all colours and types + } else { // is only semi-transparent + newTransparencyList.add(HalfTransparentBlock.class); // Slime, honey, ice, glass (is parent of AbstractGlassBlock) + } + // Both + // N/A for now + + return newTransparencyList; + } + + private static final Set> initialiseTransparentFluidSet (boolean isQuiteTransparent) { + Set> newTransparencyList = new HashSet>(); + + // if (isQuiteTransparent) { + // // N/A for now + // } else { // is only semi-transparent + // // N/A for now + // } + + // Both + newTransparencyList.add(FluidTags.WATER); // The wet stuff we all know and love + + return newTransparencyList; + } + + private static final Set> initialiseTransparentBlockSet (boolean isQuiteTransparent) { + Set> newTransparencyList = new HashSet>(); + + // if (isQuiteTransparent) { + // // N/A for now + // } else { // is only semi-transparent + // // N/A for now + // } + + // Both + // N/A for now + + return newTransparencyList; + } + + /** + * Eg: Slime, honey, ice, glass + */ + public static boolean isTransparentBlock(BlockState testBlockState) { + Block testBlock = testBlockState.getBlock(); + + for (Class transparentClass : transparentClasses) { + if (transparentClass.isAssignableFrom(testBlock.getClass())) { + return true; + } + } + + for (TagKey transparentBlock: transparentBlockTags) { + if (testBlockState.is(transparentBlock)) { + return true; + } + } + + // Else, hasn't matched anything above + return false; + } + + /** + * Eg: Water + */ + public static boolean isTransparentFluid(BlockState testBlockState) { + FluidState testFluid = testBlockState.getFluidState(); + + for (TagKey transparentFluid: transparentFluidTags) { + if (testFluid.is(transparentFluid)) { + return true; + } + } + + // Else, hasn't matched anything above + return false; + } + + /** + * Eg: Glass + */ + public static boolean isQuiteTransparentBlock(BlockState testBlockState) { + Block testBlock = testBlockState.getBlock(); + + for (Class transparentClass : quiteTransparentClasses) { + if (transparentClass.isAssignableFrom(testBlock.getClass())) { + return true; + } + } + + for (TagKey transparentBlock: quiteTransparentBlockTags) { + if (testBlockState.is(transparentBlock)) { + return true; + } + } + + // Else, hasn't matched anything above + return false; + } + /** + * Eg: Water + */ + public static boolean isQuiteTransparentFluid(BlockState testBlockState) { + FluidState testFluid = testBlockState.getFluidState(); + + for (TagKey transparentFluid: quiteTransparentFluidTags) { + if (testFluid.is(transparentFluid)) { + return true; + } + } + + // Else, hasn't matched anything above + return false; + } + + public static BlockComposition getBlockComposition(BlockState state, Level level, BlockPos pos) { + // The Level and BlockPos shouldn't actually matter to the final result + // but are required by Mojang to get the BlockState's shape + return knownBlocks.computeIfAbsent(state, (s) -> { + return new BlockComposition(s, level, pos); + }); + } + + + // External API for other mods to mark their own blocks as transparent. + // TODO: Should probably make a custom blocktag at some point specifically for folks to apply to their + // blocks rather than having block identifiers added to our list, but that's a future task for when people + // outside the BME project care enough to actually proactively make their mods work better with BME + + /** Add a new block class to mark it as transparent (transmits some light) */ + public static void addTransparentBlockClass(Class block) { transparentClasses.add(block); }; + /** Add a new block class to mark it as quite transparent (very low opacity, like glass). Block must also be marked separately as transparent */ + public static void addQuiteTransparentBlockClass(Class block) { quiteTransparentClasses.add(block); }; + + /** Add a new fluid tag to mark it as transparent (transmits some light) */ + public static void addTransparentFluidTag(TagKey fluid) { transparentFluidTags.add(fluid); }; + /** Add a new fluid tag to mark it as quite transparent (very low opacity, like water). Block must also be marked separately as transparent */ + public static void addQuiteTransparentFluidTag(TagKey fluid) { quiteTransparentFluidTags.add(fluid); }; + + /** Add a new block tag to mark it as transparent (transmits some light) */ + public static void addTransparentBlockTag(TagKey block) { transparentBlockTags.add(block); }; + /** Add a new block tag to mark it as quite transparent (very low opacity, like glass). Block must also be marked separately as transparent */ + public static void addQuiteTransparentBlockTag(TagKey block) { quiteTransparentBlockTags.add(block); }; + + + public static class BlockComposition { + public final TransparencyState totalTransparencyLevel; + public final TransparencyState blockTransparencyLevel; + public final TransparencyState fluidTransparencyLevel; + public final CompositionState compositionState; + + public BlockComposition(BlockState state, Level level, BlockPos pos) { + // Short circuit on air block. + if (state.isAir()) { + this.blockTransparencyLevel = TransparencyState.AIR; + this.fluidTransparencyLevel = TransparencyState.AIR; + this.totalTransparencyLevel = TransparencyState.AIR; + this.compositionState = CompositionState.AIR; + return; + } + + // Get shape of block to know what it occludes + VoxelShape blockShape = state.getOcclusionShape(level, pos); + boolean isBlockEmpty = blockShape.isEmpty(); + + // Set base transparency levels + if (isBlockEmpty) { + this.blockTransparencyLevel = TransparencyState.AIR; + } else if (isTransparentBlock(state)) { + if (isQuiteTransparentBlock(state)) { + this.blockTransparencyLevel = TransparencyState.QUITE_TRANSPARENT; + } else { + this.blockTransparencyLevel = TransparencyState.SEMI_TRANSPARENT; + } + } else { + this.blockTransparencyLevel = TransparencyState.OPAQUE; + } + + if (state.getFluidState().isEmpty()) { + this.fluidTransparencyLevel = TransparencyState.AIR; + } else if (isTransparentFluid(state)) { + if (isQuiteTransparentFluid(state)) { + this.fluidTransparencyLevel = TransparencyState.QUITE_TRANSPARENT; + } else { + this.fluidTransparencyLevel = TransparencyState.SEMI_TRANSPARENT; + } + } else { + this.fluidTransparencyLevel = TransparencyState.OPAQUE; + } + + // Find overall transparency state based on block shape + if (isBlockEmpty) { + // Block contains only fluid + this.compositionState = CompositionState.FLUID; + this.totalTransparencyLevel = fluidTransparencyLevel; + + } else if (Block.isFaceFull(blockShape, Direction.UP)) { + // Normal block transparency rules + this.compositionState = CompositionState.BLOCK; + this.totalTransparencyLevel = blockTransparencyLevel; + + } else if (Block.isFaceFull(blockShape, Direction.DOWN)) { + if (fluidTransparencyLevel == TransparencyState.AIR) { + // Normal block transparency rules + this.compositionState = CompositionState.BLOCK; + this.totalTransparencyLevel = blockTransparencyLevel; + + } else { + // Filter block color through fluid colour based on fluid transparency rules. + this.compositionState = CompositionState.FLUIDLOGGED_BLOCK; + + // Total opacity based on highest opacity between fluid and solid. + this.totalTransparencyLevel = TransparencyState.max(blockTransparencyLevel, fluidTransparencyLevel); + } + + } else { + // Not a solid block + + if (fluidTransparencyLevel == TransparencyState.AIR) { + // Normal block transparency rules, but can be at most semi-transparent due to + // light traveling through the gaps + this.compositionState = CompositionState.NON_FULL_BLOCK; + this.totalTransparencyLevel = TransparencyState.min(blockTransparencyLevel, TransparencyState.SEMI_TRANSPARENT); + + } else { + // Filter block color through fluid colour based on fluid transparency rules. + this.compositionState = CompositionState.FLUIDLOGGED_NON_FULL; + + // Total opacity based on fluid opacity. + // Can only cross "quite transparent" threshold if block also quite transparent + // (otherwise partial block is considering "blocking too much of the light") + this.totalTransparencyLevel = TransparencyState.max( + fluidTransparencyLevel, + TransparencyState.min(blockTransparencyLevel, TransparencyState.SEMI_TRANSPARENT) + ); + } + } + } + + public TransparencyState getTransparencyState() { return totalTransparencyLevel; } + public CompositionState getBlockCompositionState() { return compositionState; } + } + +} diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncAwaiter.java b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncAwaiter.java similarity index 94% rename from src/main/java/com/eerussianguy/blazemap/engine/async/AsyncAwaiter.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/AsyncAwaiter.java index 9b4d2cb8..659e3853 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncAwaiter.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncAwaiter.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; public class AsyncAwaiter { private final Object mutex = new Object(); diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainDelay.java b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainDelay.java similarity index 94% rename from src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainDelay.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainDelay.java index 50827b14..abe429f3 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainDelay.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainDelay.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainItem.java b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainItem.java similarity index 97% rename from src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainItem.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainItem.java index 4b02eebd..7154b11a 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainItem.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainItem.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; import java.util.Objects; import java.util.function.Function; diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainRoot.java b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainRoot.java similarity index 96% rename from src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainRoot.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainRoot.java index 64379774..cff39885 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainRoot.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainRoot.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; import java.util.function.Function; diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainTask.java b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainTask.java similarity index 93% rename from src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainTask.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainTask.java index 0f03c6a4..072a76ef 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncChainTask.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncChainTask.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; import java.util.function.Function; diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncDataCruncher.java b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncDataCruncher.java similarity index 85% rename from src/main/java/com/eerussianguy/blazemap/engine/async/AsyncDataCruncher.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/AsyncDataCruncher.java index 52fac7a2..4e65a9dc 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/AsyncDataCruncher.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/AsyncDataCruncher.java @@ -1,10 +1,10 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import com.eerussianguy.blazemap.BlazeMap; +import org.slf4j.Logger; public final class AsyncDataCruncher { private final Queue tasks = new ConcurrentLinkedQueue<>(); @@ -12,9 +12,9 @@ public final class AsyncDataCruncher { private final Object mutex = new Object(); private final LinkedList threads = new LinkedList<>(); - public AsyncDataCruncher(String name) { + public AsyncDataCruncher(String name, Logger logger) { int cores = Runtime.getRuntime().availableProcessors(); - BlazeMap.LOGGER.info("Starting {} {} AsyncDataCruncher Threads", cores, name); + logger.info("Starting {} {} AsyncDataCruncher Threads", cores, name); for(int i = 0; i < cores; i++) { Thread thread = new Thread(this::loop); thread.setName(name + " AsyncDataCruncher #" + i); @@ -22,9 +22,9 @@ public AsyncDataCruncher(String name) { thread.setPriority(7); thread.start(); threads.add(thread); - BlazeMap.LOGGER.info("Started {}", thread.getName()); + logger.info("Started {}", thread.getName()); } - BlazeMap.LOGGER.info("Started {} {} AsyncDataCruncher Threads", cores, name); + logger.info("Started {} {} AsyncDataCruncher Threads", cores, name); } public int poolSize() { diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingDelay.java b/src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingDelay.java similarity index 98% rename from src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingDelay.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingDelay.java index 02a1a472..655c16de 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingDelay.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingDelay.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; import java.util.ArrayList; diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingDomain.java b/src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingDomain.java similarity index 94% rename from src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingDomain.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingDomain.java index e972966d..3f1a9ce4 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingDomain.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingDomain.java @@ -1,12 +1,13 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; import java.util.*; import java.util.function.Consumer; -import com.eerussianguy.blazemap.BlazeMap; +import org.slf4j.Logger; public class DebouncingDomain { private static final ThreadLocal> PENDING = ThreadLocal.withInitial(ArrayList::new); + private final Logger logger; /** The DebouncingThread that makes our Domain work. */ private final DebouncingThread debouncer; @@ -26,11 +27,12 @@ public class DebouncingDomain { /** Timestamp of the next expected execution of a task within this domain. */ private long nextTaskTimestamp = Long.MAX_VALUE; - public DebouncingDomain(DebouncingThread debouncer, Consumer callback, int delayStep, int maxDelay) { + public DebouncingDomain(DebouncingThread debouncer, Consumer callback, int delayStep, int maxDelay, Logger logger) { this.debouncer = debouncer; this.callback = callback; this.delayStep = delayStep; this.maxDelay = maxDelay; + this.logger = logger; debouncer.add(this); } @@ -116,7 +118,7 @@ else if(executionTimestamp < nextTaskTimestamp) { // Find timestamp of the next callback.accept(task); } catch(Exception e) { - BlazeMap.LOGGER.error("Exception while executing pending task. Skipping"); + logger.error("Exception while executing pending task. Skipping"); e.printStackTrace(); } } diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingThread.java b/src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingThread.java similarity index 85% rename from src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingThread.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingThread.java index 18d726e8..2d3d7038 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/DebouncingThread.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/DebouncingThread.java @@ -1,21 +1,23 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; import java.util.ArrayList; import java.util.List; -import com.eerussianguy.blazemap.BlazeMap; +import org.slf4j.Logger; public class DebouncingThread { + private final Logger logger; private final Thread thread; private final List> domains; private long nextTaskTimestamp = Long.MAX_VALUE; - public DebouncingThread(String name) { + public DebouncingThread(String name, Logger logger) { + this.logger = logger; this.domains = new ArrayList<>(); this.thread = new Thread(this::loop, name + " Debouncer Thread"); thread.setDaemon(true); thread.start(); - BlazeMap.LOGGER.info("Starting {} Debouncer Thread", name); + logger.info("Starting {} Debouncer Thread", name); } /** Not public because this is meant to be called by DebouncingDomain only. */ @@ -64,7 +66,7 @@ private void work() throws InterruptedException { this.nextTaskTimestamp = nextTaskTimestamp; if(wait == 0) return; if(wait < 0) { - BlazeMap.LOGGER.error("DebouncingThread: Attempted to wait for {}ms", wait); + logger.error("DebouncingThread: Attempted to wait for {}ms", wait); wait = 100; }; @@ -84,11 +86,11 @@ private void loop() { } catch(InterruptedException ignored) {} catch(Exception e){ - BlazeMap.LOGGER.error("Exception in DebouncingThread main loop!"); + logger.error("Exception in DebouncingThread main loop!"); e.printStackTrace(); } catch(Throwable t){ - BlazeMap.LOGGER.error("Throwable in DebouncingThread main loop! ABORTING."); + logger.error("Throwable in DebouncingThread main loop! ABORTING."); t.printStackTrace(); return; } diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/IThreadQueue.java b/src/main/java/com/eerussianguy/blazemap/lib/async/IThreadQueue.java similarity index 64% rename from src/main/java/com/eerussianguy/blazemap/engine/async/IThreadQueue.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/IThreadQueue.java index a8547e43..c6b6c78b 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/IThreadQueue.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/IThreadQueue.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; @FunctionalInterface public interface IThreadQueue { diff --git a/src/main/java/com/eerussianguy/blazemap/engine/async/PriorityLock.java b/src/main/java/com/eerussianguy/blazemap/lib/async/PriorityLock.java similarity index 97% rename from src/main/java/com/eerussianguy/blazemap/engine/async/PriorityLock.java rename to src/main/java/com/eerussianguy/blazemap/lib/async/PriorityLock.java index dc44ecb7..deb00ae4 100644 --- a/src/main/java/com/eerussianguy/blazemap/engine/async/PriorityLock.java +++ b/src/main/java/com/eerussianguy/blazemap/lib/async/PriorityLock.java @@ -1,4 +1,4 @@ -package com.eerussianguy.blazemap.engine.async; +package com.eerussianguy.blazemap.lib.async; public class PriorityLock { private boolean priorityWaiting; diff --git a/src/main/java/com/eerussianguy/blazemap/lib/gui/components/HueSlider.java b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/HueSlider.java new file mode 100644 index 00000000..1430b900 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/HueSlider.java @@ -0,0 +1,29 @@ +package com.eerussianguy.blazemap.lib.gui.components; + +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.mojang.blaze3d.vertex.PoseStack; + +public class HueSlider extends Slider { + int color = 0xFF404040; + + {step = 1F / 72F;} // not something you see every day, is it? + + protected void renderBackground(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + stack.pushPose(); + renderFocusableFlatBackground(stack); + stack.translate(1, 1, 0); + RenderHelper.renderChromaticGradient(stack, getWidth() - 2, getHeight() - 2); + stack.popPose(); + } + + @Override // dynamic handle color + public int getHandleColor() { + return color; + } + + public Slider setValue(float value) { + color = Colors.HSB2RGB(value, 1, 1); + return super.setValue(value); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/gui/components/IconTabs.java b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/IconTabs.java new file mode 100644 index 00000000..2cf9098b --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/IconTabs.java @@ -0,0 +1,156 @@ +package com.eerussianguy.blazemap.lib.gui.components; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.eerussianguy.blazemap.lib.gui.core.Positionable; +import com.eerussianguy.blazemap.lib.gui.core.TooltipService; +import com.eerussianguy.blazemap.lib.gui.trait.BorderedComponent; +import com.eerussianguy.blazemap.lib.gui.trait.ComponentSounds; +import com.mojang.blaze3d.vertex.PoseStack; + +public class IconTabs extends BaseComponent implements BorderedComponent, ComponentSounds, GuiEventListener { + private final ArrayList tabs = new ArrayList<>(); + private int size, grain, track; + private int begin, end; + private Tab active = null; + + public IconTabs() { + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + //render bottom line + stack.pushPose(); + stack.translate(-begin, getHeight() - 1, 0); + RenderHelper.fillRect(stack.last().pose(), getWidth() + begin + end, 1, Colors.UNFOCUSED); + stack.popPose(); + + // render tabs + for(var tab : tabs) { + stack.pushPose(); + stack.translate(tab.getPositionX(), tab.getPositionY(), 0); + var item = tab.component; + + if(tab == active) { // render active tab + renderBorderedBox(stack, -1, -1, tab.getWidth()+2, tab.getHeight()+2, Colors.UNFOCUSED, Colors.BLACK); + stack.pushPose(); + stack.translate(0, tab.getHeight(), 0); + RenderHelper.fillRect(stack.last().pose(), tab.getWidth(), 1, 0xFF303030); + stack.popPose(); + Minecraft.getInstance().font.draw(stack, item.getName(), size + 2, size / 2f - 4, Colors.WHITE); + } + else { // render inactive tabs + renderBorderedBox(stack, -1, 0, tab.getWidth()+2, tab.getHeight()+1, Colors.UNFOCUSED, Colors.BLACK); + + // hover only for inactive tabs + if(hasMouse && tab.mouseIntercepts(mouseX, mouseY)) { + stack.pushPose(); + stack.translate(0, 1, 0); + RenderHelper.fillRect(stack.last().pose(), tab.getWidth(), tab.getHeight()-1, 0xFF222222); // render hover + stack.popPose(); + } + } + + // render icon + RenderHelper.drawTexturedQuad(item.getIcon(), item.getIconTint(), stack, 1, 1, size-2, size-2); + + stack.popPose(); + } + } + + @Override + protected void renderTooltip(PoseStack stack, int mouseX, int mouseY, TooltipService service) { + for(var tab : tabs) { + if(tab.mouseIntercepts(mouseX, mouseY)) { + service.drawTooltip(stack, mouseX, mouseY, tab.component.getTooltip()); + return; + } + } + } + + @Override + public IconTabs setSize(int w, int h) { + super.setSize(w, h); + size = h - 2; + grain = h - 1; + track = w - 2; + recalculate(); + return this; + } + + private void recalculate() { + if(tabs.size() == 0) return; + int open = track - (tabs.size() - 1) * grain; + int x = 1; + for(var tab : tabs) { + tab.setSize(tab == active ? open : size, size); + tab.setPosition(x, 1); + x += tab.getWidth()+1; + } + } + + public IconTabs setLine(int begin, int end) { + this.begin = begin; + this.end = end; + return this; + } + + public void add(TabComponent component) { + Tab tab = new Tab(component); + tabs.add(tab); + if(active == null) { + active = tab; + } + component.setVisible(active == tab); + recalculate(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for(var tab : tabs) { + if(tab.mouseIntercepts(mouseX, mouseY)) { + if(tab == active) { + playDeniedSound(); + } + else { + setActive(tab); + playOkSound(); + recalculate(); + } + break; + } + } + return true; + } + + private void setActive(Tab tab) { + if(active != null) active.component.setVisible(false); + tab.component.setVisible(true); + active = tab; + } + + public interface TabComponent { + void setVisible(boolean visible); + ResourceLocation getIcon(); + int getIconTint(); + Component getName(); + List getTooltip(); + } + + public static class Tab extends Positionable { + private final TabComponent component; + + public Tab(TabComponent component) { + this.component = component; + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/gui/components/Image.java b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/Image.java new file mode 100644 index 00000000..d7c1ff9e --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/Image.java @@ -0,0 +1,43 @@ +package com.eerussianguy.blazemap.lib.gui.components; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.eerussianguy.blazemap.lib.gui.core.TooltipService; +import com.mojang.blaze3d.vertex.PoseStack; + +public class Image extends BaseComponent { + private final ResourceLocation image; + private int color = Colors.NO_TINT; + private Component tooltip; + + public Image(ResourceLocation image, int width, int height) { + this.setSize(width, height); + this.image = image; + } + + public Image color(int color) { + this.color = color; + return this; + } + + public Image tooltip(Component tooltip) { + this.tooltip = tooltip; + return this; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + RenderHelper.drawTexturedQuad(image, color, stack, 0, 0, getWidth(), getHeight()); + } + + @Override + protected void renderTooltip(PoseStack stack, int mouseX, int mouseY, TooltipService service) { + if(tooltip != null) { + service.drawTooltip(stack, mouseX, mouseY, tooltip); + } + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/gui/components/ImageButton.java b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/ImageButton.java new file mode 100644 index 00000000..9ebc6e1d --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/ImageButton.java @@ -0,0 +1,29 @@ +package com.eerussianguy.blazemap.lib.gui.components; + +import java.util.function.IntConsumer; + +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.core.BaseButton; +import com.mojang.blaze3d.vertex.PoseStack; + +public class ImageButton extends BaseButton { + protected final ResourceLocation background; + + public ImageButton(ResourceLocation background, int width, int height, IntConsumer function) { + super(function); + this.background = background; + this.setSize(width, height); + } + + protected int getTint() { + return Colors.NO_TINT; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + RenderHelper.drawTexturedQuad(background, getTint(), stack, 0, 0, getWidth(), getHeight()); + } +} diff --git a/src/main/java/com/eerussianguy/blazemap/lib/gui/components/ImageDisplay.java b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/ImageDisplay.java new file mode 100644 index 00000000..3b6d2721 --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/ImageDisplay.java @@ -0,0 +1,45 @@ +package com.eerussianguy.blazemap.lib.gui.components; + +import java.util.function.IntSupplier; + +import net.minecraft.resources.ResourceLocation; + +import com.eerussianguy.blazemap.lib.RenderHelper; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.eerussianguy.blazemap.lib.gui.trait.BorderedComponent; +import com.mojang.blaze3d.vertex.PoseStack; + +public class ImageDisplay extends BaseComponent implements BorderedComponent { + private ResourceLocation image; + private int imageWidth, imageHeight; + private IntSupplier color; + + public ImageDisplay setImageSize(int w, int h) { + this.imageWidth = w; + this.imageHeight = h; + return this; + } + + public ImageDisplay setColor(int color) { + return setColor(() -> color); + } + + public ImageDisplay setColor(IntSupplier color) { + this.color = color; + return this; + } + + public ImageDisplay setImage(ResourceLocation image) { + this.image = image; + return this; + } + + @Override + public void render(PoseStack stack, boolean hasMouse, int mouseX, int mouseY) { + this.renderBorderedBackground(stack); + + if(image == null) return; + + RenderHelper.drawTexturedQuad(image, color.getAsInt(), stack, (getWidth() - imageWidth) / 2, (getHeight() - imageHeight) / 2, imageWidth, imageHeight); + } +} \ No newline at end of file diff --git a/src/main/java/com/eerussianguy/blazemap/lib/gui/components/Label.java b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/Label.java new file mode 100644 index 00000000..3920278d --- /dev/null +++ b/src/main/java/com/eerussianguy/blazemap/lib/gui/components/Label.java @@ -0,0 +1,75 @@ +package com.eerussianguy.blazemap.lib.gui.components; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; + +import com.eerussianguy.blazemap.lib.Colors; +import com.eerussianguy.blazemap.lib.gui.core.BaseComponent; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.datafixers.util.Either; + +public class Label extends BaseComponent