diff --git a/build.gradle b/build.gradle index 1a247a4e5..d6bcadce0 100644 --- a/build.gradle +++ b/build.gradle @@ -141,6 +141,22 @@ repositories { name = "squiddev" url = "https://squiddev.cc/maven/" } + maven { + name = "ftb" + url = "https://maven.saps.dev/releases" + } + maven { + name = "architectury" + url = "https://maven.architectury.dev/" + } + maven { + url = "https://jm.gserv.me/repository/maven-public/" + content { + includeGroup "info.journeymap" + includeGroup "mysticdrew" + } + } + maven { url = 'https://www.cursemaven.com' @@ -200,6 +216,14 @@ dependencies { // implementation fg.deobf("curse.maven:ic2-classic-242942:5555152") // implementation fg.deobf("curse.maven:druidcraft-340991:3101903") // implementation fg.deobf("com.railwayteam.railways:railways-1.19.2-1.6.4:all") { transitive = false } + + implementation fg.deobf("dev.architectury:architectury-forge:9.1.12") + implementation fg.deobf("dev.ftb.mods:ftb-chunks-forge:2001.3.1") + implementation fg.deobf("dev.ftb.mods:ftb-teams-forge:2001.3.0") + implementation fg.deobf("dev.ftb.mods:ftb-library-forge:2001.2.4") + + implementation fg.deobf("curse.maven:journeymap-32274:5457831") + // implementation fg.deobf("ignored:journeymap-1.20.1-5.10.1-forge") // runtimeOnly fg.deobf("curse.maven:framedblocks-441647:5399211") // runtimeOnly fg.deobf("curse.maven:galosphere-631098:4574834") diff --git a/gradle.properties b/gradle.properties index 78586a208..dd23f624a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ mod_version = 0.5.1.g artifact_minecraft_version = 1.20.1 minecraft_version = 1.20.1 -forge_version = 47.1.43 +forge_version = 47.1.47 # build dependency versions forgegradle_version = 6.0.6 diff --git a/src/generated/resources/.cache/2d64935085b86659cb7857bad9701dbf9bab6e4c b/src/generated/resources/.cache/2d64935085b86659cb7857bad9701dbf9bab6e4c index 59ca98997..b01560667 100644 --- a/src/generated/resources/.cache/2d64935085b86659cb7857bad9701dbf9bab6e4c +++ b/src/generated/resources/.cache/2d64935085b86659cb7857bad9701dbf9bab6e4c @@ -1,4 +1,4 @@ -// 1.20.1 2024-08-08T08:37:57.7244288 Registrate Provider for create [Recipes, Advancements, Loot Tables, Tags (blocks), Tags (items), Tags (fluids), Tags (entity_types), Blockstates, Item models, Lang (en_us/en_ud)] +// 1.20.1 2024-09-03T11:32:11.6637155 Registrate Provider for create [Recipes, Advancements, Loot Tables, Tags (blocks), Tags (items), Tags (fluids), Tags (entity_types), Blockstates, Item models, Lang (en_us/en_ud)] 60bbdf92d2ac9824ea6144955c74043a6005f79d assets/create/blockstates/acacia_window.json 6a67703c2697d81b7dc83e9d72a66f9c9ff08383 assets/create/blockstates/acacia_window_pane.json c3ae87b62e81d8e9476eccd793bb1548d74c66a1 assets/create/blockstates/adjustable_chain_gearshift.json @@ -585,8 +585,8 @@ b0d8f08968763a5f74e5cd5644377a76a9f39753 assets/create/blockstates/yellow_toolbo fe8c497aacc641c2f01cec90bba9f19e59cc2ed2 assets/create/blockstates/yellow_valve_handle.json e819e93fdcbe9fd9c050a052d2718ff3b3539365 assets/create/blockstates/zinc_block.json 64121dcb216381c83b4fe28aa361ea07c24c9ad0 assets/create/blockstates/zinc_ore.json -b9295b12fb7ba72de2f245064305b9dd4d6f9ce1 assets/create/lang/en_ud.json -16147eb4e472f1e670bcfc65a13ebcda0c8f58b8 assets/create/lang/en_us.json +1195fdc4fb51659c921e2bbe744a35107f787aa2 assets/create/lang/en_ud.json +632d1aac7255fc0f4804f4df138ce9926134d2f9 assets/create/lang/en_us.json a97e1060e00ae701a02e39cd4ef8054cf345fac4 assets/create/models/block/acacia_window.json 103e032c0b1a0a6a27c67da8c91179a564bd281c assets/create/models/block/acacia_window_pane_noside.json fb00b627abda76ad4fea867ca57dbfadd24fffa3 assets/create/models/block/acacia_window_pane_noside_alt.json diff --git a/src/generated/resources/assets/create/lang/en_ud.json b/src/generated/resources/assets/create/lang/en_ud.json index d08f0112a..302cd5815 100644 --- a/src/generated/resources/assets/create/lang/en_ud.json +++ b/src/generated/resources/assets/create/lang/en_ud.json @@ -2479,6 +2479,7 @@ "create.station.remove_auto_schedule": "ǝןnpǝɥɔS-oʇnⱯ pɹɐɔsıᗡ", "create.station.remove_schedule": "ǝןnpǝɥɔS ǝʌǝıɹʇǝᴚ", "create.station.retry": "ʎɹʇǝɹ puɐ sıɥʇ ǝʌןosǝᴚ", + "create.station.train_map_color": "sdɐW uo ɹoןoƆ", "create.station.train_not_aligned": "'ǝןqɯǝssɐsıp ʇouuɐƆ", "create.station.train_not_aligned_1": "pǝubıןɐ sǝbɐıɹɹɐɔ ןןɐ ʇou", "create.subtitle.blaze_munch": "sǝɥɔunɯ ɹǝuɹnᗺ ǝzɐןᗺ", @@ -2649,6 +2650,21 @@ "create.train_assembly.sideways_controls": "sʎɐʍǝpıs ǝɔɐɟ ʇouuɐɔ sןoɹʇuoƆ uıɐɹ⟘", "create.train_assembly.single_bogey_carriage": "uʍo sʇı uo ǝbɐıɹɹɐɔ ɐ ʇɹoddns ʇouuɐɔ ǝdʎʇ ʎǝboᗺ sıɥ⟘", "create.train_assembly.too_many_bogeys": "%1$s :pǝɥɔɐʇʇɐ sʎǝboᗺ ʎuɐɯ oo⟘", + "create.train_map.cannot_traverse_section": "ǝsɹǝʌɐɹʇ ʎןןnɟ ʇouuɐƆ ", + "create.train_map.conductor_missing": "buıssıW ɹoʇɔnpuoƆ >¡< ", + "create.train_map.derailed": "pǝןıɐɹǝᗡ >¡< ", + "create.train_map.for_other_train": "%1$s ɹoɟ ", + "create.train_map.fuel_boosted": "✔ pǝʇsooq ןǝnℲ ", + "create.train_map.navigation_failed": "pǝןıɐℲ uoıʇɐbıʌɐN >¡< ", + "create.train_map.player_controlled": "ɹǝʎɐןԀ ʎq pǝןןoɹʇuoƆ >- ", + "create.train_map.redstone_powered": "pǝɹǝʍoԀ ǝuoʇspǝᴚ ", + "create.train_map.schedule_interrupted": "pǝʇdnɹɹǝʇuI ǝןnpǝɥɔS >¡< ", + "create.train_map.section_reserved": "pǝʌɹǝsǝɹ uoıʇɔǝS ", + "create.train_map.toggle": "ʎɐןɹǝʌo ʞɹoʍʇǝu uıɐɹ⟘", + "create.train_map.train_at_station": "%1$s |> ", + "create.train_map.train_moving_to_station": ")ɯ%2$s( %1$s >> ", + "create.train_map.train_owned_by": "%1$s ʎq", + "create.train_map.waiting_at_signal": "ןɐubıS ʇɐ buıʇıɐM ", "create.tunnel.selection_mode.forced_round_robin": "uıqoᴚ punoᴚ pǝɔɹoℲ", "create.tunnel.selection_mode.forced_split": "ʇıןdS pǝɔɹoℲ", "create.tunnel.selection_mode.prefer_nearest": "ʇsǝɹɐǝN ɹǝɟǝɹԀ", diff --git a/src/generated/resources/assets/create/lang/en_us.json b/src/generated/resources/assets/create/lang/en_us.json index aaf18c624..a166b2fba 100644 --- a/src/generated/resources/assets/create/lang/en_us.json +++ b/src/generated/resources/assets/create/lang/en_us.json @@ -2479,6 +2479,7 @@ "create.station.remove_auto_schedule": "Discard Auto-Schedule", "create.station.remove_schedule": "Retrieve Schedule", "create.station.retry": "Resolve this and retry", + "create.station.train_map_color": "Color on Maps", "create.station.train_not_aligned": "Cannot disassemble,", "create.station.train_not_aligned_1": "not all carriages aligned", "create.subtitle.blaze_munch": "Blaze Burner munches", @@ -2649,6 +2650,21 @@ "create.train_assembly.sideways_controls": "Train Controls cannot face sideways", "create.train_assembly.single_bogey_carriage": "This Bogey type cannot support a carriage on its own", "create.train_assembly.too_many_bogeys": "Too many Bogeys attached: %1$s", + "create.train_map.cannot_traverse_section": " Cannot fully traverse", + "create.train_map.conductor_missing": " Conductor Missing", + "create.train_map.derailed": " Derailed", + "create.train_map.for_other_train": " for %1$s", + "create.train_map.fuel_boosted": " Fuel boosted ✔", + "create.train_map.navigation_failed": " Navigation Failed", + "create.train_map.player_controlled": " -> Controlled by Player", + "create.train_map.redstone_powered": " Redstone Powered", + "create.train_map.schedule_interrupted": " Schedule Interrupted", + "create.train_map.section_reserved": " Section reserved", + "create.train_map.toggle": "Train network overlay", + "create.train_map.train_at_station": " >| %1$s", + "create.train_map.train_moving_to_station": " >> %1$s (%2$sm)", + "create.train_map.train_owned_by": "by %1$s", + "create.train_map.waiting_at_signal": " Waiting at Signal", "create.tunnel.selection_mode.forced_round_robin": "Forced Round Robin", "create.tunnel.selection_mode.forced_split": "Forced Split", "create.tunnel.selection_mode.prefer_nearest": "Prefer Nearest", diff --git a/src/main/java/com/simibubi/create/AllPackets.java b/src/main/java/com/simibubi/create/AllPackets.java index 3c68ae47e..412375280 100644 --- a/src/main/java/com/simibubi/create/AllPackets.java +++ b/src/main/java/com/simibubi/create/AllPackets.java @@ -8,6 +8,8 @@ import java.util.function.Function; import java.util.function.Supplier; import com.simibubi.create.compat.computercraft.AttachedComputerPacket; +import com.simibubi.create.compat.trainmap.TrainMapSyncPacket; +import com.simibubi.create.compat.trainmap.TrainMapSyncRequestPacket; import com.simibubi.create.content.contraptions.ContraptionBlockChangedPacket; import com.simibubi.create.content.contraptions.ContraptionColliderLockPacket; import com.simibubi.create.content.contraptions.ContraptionColliderLockPacket.ContraptionColliderLockPacketRequest; @@ -164,6 +166,7 @@ public enum AllPackets { CLIPBOARD_EDIT(ClipboardEditPacket.class, ClipboardEditPacket::new, PLAY_TO_SERVER), CONTRAPTION_COLLIDER_LOCK_REQUEST(ContraptionColliderLockPacketRequest.class, ContraptionColliderLockPacketRequest::new, PLAY_TO_SERVER), + TRAIN_MAP_REQUEST(TrainMapSyncRequestPacket.class, TrainMapSyncRequestPacket::new, PLAY_TO_SERVER), // Server to Client SYMMETRY_EFFECT(SymmetryEffectPacket.class, SymmetryEffectPacket::new, PLAY_TO_CLIENT), @@ -208,7 +211,8 @@ public enum AllPackets { CONTRAPTION_ACTOR_TOGGLE(ContraptionDisableActorPacket.class, ContraptionDisableActorPacket::new, PLAY_TO_CLIENT), CONTRAPTION_COLLIDER_LOCK(ContraptionColliderLockPacket.class, ContraptionColliderLockPacket::new, PLAY_TO_CLIENT), ATTACHED_COMPUTER(AttachedComputerPacket.class, AttachedComputerPacket::new, PLAY_TO_CLIENT), - SERVER_DEBUG_INFO(ServerDebugInfoPacket.class, ServerDebugInfoPacket::new, PLAY_TO_CLIENT) + SERVER_DEBUG_INFO(ServerDebugInfoPacket.class, ServerDebugInfoPacket::new, PLAY_TO_CLIENT), + TRAIN_MAP_SYNC(TrainMapSyncPacket.class, TrainMapSyncPacket::new, PLAY_TO_CLIENT) ; public static final ResourceLocation CHANNEL_NAME = Create.asResource("main"); diff --git a/src/main/java/com/simibubi/create/compat/Mods.java b/src/main/java/com/simibubi/create/compat/Mods.java index a89dce5ff..a78d2b780 100644 --- a/src/main/java/com/simibubi/create/compat/Mods.java +++ b/src/main/java/com/simibubi/create/compat/Mods.java @@ -31,7 +31,9 @@ public enum Mods { TCONSTRUCT, FRAMEDBLOCKS, XLPACKETS, - MODERNUI; + MODERNUI, + FTBCHUNKS, + JOURNEYMAP; private final String id; diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StationPeripheral.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StationPeripheral.java index c50891fe3..146742b3b 100644 --- a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StationPeripheral.java +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StationPeripheral.java @@ -129,7 +129,7 @@ public class StationPeripheral extends SyncedPeripheral { public final void setTrainName(String name) throws LuaException { Train train = getTrainOrThrow(); train.name = Components.literal(name); - AllPackets.getChannel().send(PacketDistributor.ALL.noArg(), new TrainEditPacket.TrainEditReturnPacket(train.id, name, train.icon.getId())); + AllPackets.getChannel().send(PacketDistributor.ALL.noArg(), new TrainEditPacket.TrainEditReturnPacket(train.id, name, train.icon.getId(), train.mapColorIndex)); } @LuaFunction diff --git a/src/main/java/com/simibubi/create/compat/trainmap/FTBChunksTrainMap.java b/src/main/java/com/simibubi/create/compat/trainmap/FTBChunksTrainMap.java new file mode 100644 index 000000000..f60921ae4 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/FTBChunksTrainMap.java @@ -0,0 +1,158 @@ +package com.simibubi.create.compat.trainmap; + +import java.util.List; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.foundation.gui.RemovedGuiUtils; +import com.simibubi.create.foundation.utility.Lang; +import com.simibubi.create.infrastructure.config.AllConfigs; + +import dev.ftb.mods.ftbchunks.client.gui.LargeMapScreen; +import dev.ftb.mods.ftbchunks.client.gui.RegionMapPanel; +import dev.ftb.mods.ftblibrary.ui.ScreenWrapper; +import dev.ftb.mods.ftblibrary.ui.Widget; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.Rect2i; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.util.Mth; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RenderTooltipEvent; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.fml.util.ObfuscationReflectionHelper; + +public class FTBChunksTrainMap { + + private static int cancelTooltips = 0; + private static boolean renderingTooltip = false; + private static boolean requesting; + + public static void tick() { + if (cancelTooltips > 0) + cancelTooltips--; + if (!AllConfigs.client().showTrainMapOverlay.get() + || getAsLargeMapScreen(Minecraft.getInstance().screen) == null) { + if (requesting) + TrainMapSyncClient.stopRequesting(); + requesting = false; + return; + } + TrainMapManager.tick(); + requesting = true; + TrainMapSyncClient.requestData(); + } + + public static void cancelTooltips(RenderTooltipEvent.Pre event) { + if (getAsLargeMapScreen(Minecraft.getInstance().screen) == null) + return; + if (renderingTooltip || cancelTooltips == 0) + return; + event.setCanceled(true); + } + + public static void mouseClick(InputEvent.MouseButton.Pre event) { + LargeMapScreen screen = getAsLargeMapScreen(Minecraft.getInstance().screen); + if (screen == null) + return; + if (TrainMapManager.handleToggleWidgetClick(screen.getMouseX(), screen.getMouseY(), 20, 2)) + event.setCanceled(true); + } + + public static void renderGui(ScreenEvent.Render.Post event) { + LargeMapScreen largeMapScreen = getAsLargeMapScreen(event.getScreen()); + if (largeMapScreen == null) + return; + Object panel = ObfuscationReflectionHelper.getPrivateValue(LargeMapScreen.class, largeMapScreen, "regionPanel"); + if (!(panel instanceof RegionMapPanel regionMapPanel)) + return; + GuiGraphics graphics = event.getGuiGraphics(); + if (!AllConfigs.client().showTrainMapOverlay.get()) { + renderToggleWidgetAndTooltip(event, largeMapScreen, graphics); + return; + } + + int blocksPerRegion = 16 * 32; + int minX = Mth.floor(regionMapPanel.getScrollX()); + int minY = Mth.floor(regionMapPanel.getScrollY()); + float regionTileSize = largeMapScreen.getRegionTileSize() / (float) blocksPerRegion; + int regionMinX = + ObfuscationReflectionHelper.getPrivateValue(RegionMapPanel.class, regionMapPanel, "regionMinX"); + int regionMinZ = + ObfuscationReflectionHelper.getPrivateValue(RegionMapPanel.class, regionMapPanel, "regionMinZ"); + float mouseX = event.getMouseX(); + float mouseY = event.getMouseY(); + + boolean linearFiltering = largeMapScreen.getRegionTileSize() * Minecraft.getInstance() + .getWindow() + .getGuiScale() < 512D; + + PoseStack pose = graphics.pose(); + pose.pushPose(); + + pose.translate(-minX, -minY, 0); + pose.scale(regionTileSize, regionTileSize, 1); + pose.translate(-regionMinX * blocksPerRegion, -regionMinZ * blocksPerRegion, 0); + + mouseX += minX; + mouseY += minY; + mouseX /= regionTileSize; + mouseY /= regionTileSize; + mouseX += regionMinX * blocksPerRegion; + mouseY += regionMinZ * blocksPerRegion; + + Rect2i bounds = new Rect2i(Mth.floor(minX / regionTileSize + regionMinX * blocksPerRegion), + Mth.floor(minY / regionTileSize + regionMinZ * blocksPerRegion), + Mth.floor(largeMapScreen.width / regionTileSize), Mth.floor(largeMapScreen.height / regionTileSize)); + + List tooltip = TrainMapManager.renderAndPick(graphics, Mth.floor(mouseX), Mth.floor(mouseY), + event.getPartialTick(), linearFiltering, bounds); + + pose.popPose(); + + if (!renderToggleWidgetAndTooltip(event, largeMapScreen, graphics) && tooltip != null) { + renderingTooltip = true; + RemovedGuiUtils.drawHoveringText(graphics, tooltip, event.getMouseX(), event.getMouseY(), + largeMapScreen.width, largeMapScreen.height, 256, Minecraft.getInstance().font); + renderingTooltip = false; + cancelTooltips = 5; + } + + pose.pushPose(); + pose.translate(0, 0, 300); + for (Widget widget : largeMapScreen.getWidgets()) { + if (!widget.isEnabled()) + continue; + if (widget == panel) + continue; + widget.draw(graphics, largeMapScreen.getTheme(), widget.getPosX(), widget.getPosY(), widget.getWidth(), + widget.getHeight()); + } + pose.popPose(); + } + + private static boolean renderToggleWidgetAndTooltip(ScreenEvent.Render.Post event, LargeMapScreen largeMapScreen, + GuiGraphics graphics) { + TrainMapManager.renderToggleWidget(graphics, 20, 2); + if (!TrainMapManager.isToggleWidgetHovered(event.getMouseX(), event.getMouseY(), 20, 2)) + return false; + + renderingTooltip = true; + RemovedGuiUtils.drawHoveringText(graphics, List.of(Lang.translate("train_map.toggle") + .component()), event.getMouseX(), event.getMouseY() + 20, largeMapScreen.width, largeMapScreen.height, 256, + Minecraft.getInstance().font); + renderingTooltip = false; + cancelTooltips = 5; + return true; + } + + private static LargeMapScreen getAsLargeMapScreen(Screen screen) { + if (!(screen instanceof ScreenWrapper screenWrapper)) + return null; + Object wrapped = ObfuscationReflectionHelper.getPrivateValue(ScreenWrapper.class, screenWrapper, "wrappedGui"); + if (!(wrapped instanceof LargeMapScreen largeMapScreen)) + return null; + return largeMapScreen; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/trainmap/JourneyTrainMap.java b/src/main/java/com/simibubi/create/compat/trainmap/JourneyTrainMap.java new file mode 100644 index 000000000..74beed9a5 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/JourneyTrainMap.java @@ -0,0 +1,108 @@ +package com.simibubi.create.compat.trainmap; + +import java.util.List; + +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.foundation.gui.RemovedGuiUtils; +import com.simibubi.create.foundation.utility.Lang; +import com.simibubi.create.infrastructure.config.AllConfigs; + +import journeymap.client.api.display.Context.UI; +import journeymap.client.api.util.UIState; +import journeymap.client.ui.fullscreen.Fullscreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.Rect2i; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.util.Mth; +import net.minecraftforge.client.event.InputEvent; + +public class JourneyTrainMap { + + private static boolean requesting; + + public static void tick() { + if (!AllConfigs.client().showTrainMapOverlay.get() || !(Minecraft.getInstance().screen instanceof Fullscreen)) { + if (requesting) + TrainMapSyncClient.stopRequesting(); + requesting = false; + return; + } + TrainMapManager.tick(); + requesting = true; + TrainMapSyncClient.requestData(); + } + + public static void mouseClick(InputEvent.MouseButton.Pre event) { + Minecraft mc = Minecraft.getInstance(); + if (!(mc.screen instanceof Fullscreen screen)) + return; + + Window window = mc.getWindow(); + double mX = mc.mouseHandler.xpos() * window.getGuiScaledWidth() / window.getScreenWidth(); + double mY = mc.mouseHandler.ypos() * window.getGuiScaledHeight() / window.getScreenHeight(); + + if (TrainMapManager.handleToggleWidgetClick(Mth.floor(mX), Mth.floor(mY), 3, 30)) + event.setCanceled(true); + } + + // Called by JourneyFullscreenMapMixin + public static void onRender(GuiGraphics graphics, Fullscreen screen, double x, double z, int mX, int mY, float pt) { + UIState state = screen.getUiState(); + if (state == null) + return; + if (state.ui != UI.Fullscreen) + return; + if (!state.active) + return; + if (!AllConfigs.client().showTrainMapOverlay.get()) { + renderToggleWidgetAndTooltip(graphics, screen, mX, mY); + return; + } + + Minecraft mc = Minecraft.getInstance(); + Window window = mc.getWindow(); + + double guiScale = (double) window.getScreenWidth() / window.getGuiScaledWidth(); + double scale = state.blockSize / guiScale; + + PoseStack pose = graphics.pose(); + pose.pushPose(); + + pose.translate(screen.width / 2.0f, screen.height / 2.0f, 0); + pose.scale((float) scale, (float) scale, 1); + pose.translate(-x, -z, 0); + + float mouseX = mX - screen.width / 2.0f; + float mouseY = mY - screen.height / 2.0f; + mouseX /= scale; + mouseY /= scale; + mouseX += x; + mouseY += z; + + Rect2i bounds = + new Rect2i(Mth.floor(-screen.width / 2.0f / scale + x), Mth.floor(-screen.height / 2.0f / scale + z), + Mth.floor(screen.width / scale), Mth.floor(screen.height / scale)); + + List tooltip = + TrainMapManager.renderAndPick(graphics, Mth.floor(mouseX), Mth.floor(mouseY), pt, false, bounds); + + pose.popPose(); + + if (!renderToggleWidgetAndTooltip(graphics, screen, mX, mY) && tooltip != null) + RemovedGuiUtils.drawHoveringText(graphics, tooltip, mX, mY, screen.width, screen.height, 256, mc.font); + } + + private static boolean renderToggleWidgetAndTooltip(GuiGraphics graphics, Fullscreen screen, int mouseX, + int mouseY) { + TrainMapManager.renderToggleWidget(graphics, 3, 30); + if (!TrainMapManager.isToggleWidgetHovered(mouseX, mouseY, 3, 30)) + return false; + + RemovedGuiUtils.drawHoveringText(graphics, List.of(Lang.translate("train_map.toggle") + .component()), mouseX, mouseY + 20, screen.width, screen.height, 256, Minecraft.getInstance().font); + return true; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/trainmap/TrainMapEvents.java b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapEvents.java new file mode 100644 index 000000000..54f2adb19 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapEvents.java @@ -0,0 +1,56 @@ +package com.simibubi.create.compat.trainmap; + +import com.mojang.blaze3d.platform.InputConstants; +import com.simibubi.create.compat.Mods; + +import net.minecraft.client.Minecraft; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RenderTooltipEvent; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.event.TickEvent.ClientTickEvent; +import net.minecraftforge.event.TickEvent.Phase; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; + +@EventBusSubscriber(value = Dist.CLIENT) +public class TrainMapEvents { + + @SubscribeEvent + public static void tick(ClientTickEvent event) { + if (event.phase == Phase.START) + return; + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) + return; + + if (Mods.FTBCHUNKS.isLoaded()) + FTBChunksTrainMap.tick(); + if (Mods.JOURNEYMAP.isLoaded()) + JourneyTrainMap.tick(); + } + + @SubscribeEvent + public static void mouseClick(InputEvent.MouseButton.Pre event) { + if (event.getAction() != InputConstants.PRESS) + return; + + if (Mods.FTBCHUNKS.isLoaded()) + FTBChunksTrainMap.mouseClick(event); + if (Mods.JOURNEYMAP.isLoaded()) + JourneyTrainMap.mouseClick(event); + } + + @SubscribeEvent + public static void cancelTooltips(RenderTooltipEvent.Pre event) { + if (Mods.FTBCHUNKS.isLoaded()) + FTBChunksTrainMap.cancelTooltips(event); + } + + @SubscribeEvent + public static void renderGui(ScreenEvent.Render.Post event) { + if (Mods.FTBCHUNKS.isLoaded()) + FTBChunksTrainMap.renderGui(event); + } + +} diff --git a/src/main/java/com/simibubi/create/compat/trainmap/TrainMapManager.java b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapManager.java new file mode 100644 index 000000000..04ddf727a --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapManager.java @@ -0,0 +1,730 @@ +package com.simibubi.create.compat.trainmap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import com.simibubi.create.CreateClient; +import com.simibubi.create.compat.trainmap.TrainMapSync.SignalState; +import com.simibubi.create.compat.trainmap.TrainMapSync.TrainMapSyncEntry; +import com.simibubi.create.compat.trainmap.TrainMapSync.TrainState; +import com.simibubi.create.content.trains.entity.Carriage; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.graph.EdgePointType; +import com.simibubi.create.content.trains.graph.TrackEdge; +import com.simibubi.create.content.trains.graph.TrackGraph; +import com.simibubi.create.content.trains.graph.TrackNode; +import com.simibubi.create.content.trains.graph.TrackNodeLocation; +import com.simibubi.create.content.trains.station.GlobalStation; +import com.simibubi.create.content.trains.track.BezierConnection; +import com.simibubi.create.foundation.gui.AllGuiTextures; +import com.simibubi.create.foundation.utility.AnimationTickHolder; +import com.simibubi.create.foundation.utility.Components; +import com.simibubi.create.foundation.utility.Couple; +import com.simibubi.create.foundation.utility.Iterate; +import com.simibubi.create.foundation.utility.Lang; +import com.simibubi.create.foundation.utility.Pair; +import com.simibubi.create.infrastructure.config.AllConfigs; +import com.simibubi.create.infrastructure.config.CClient; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.Rect2i; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +public class TrainMapManager { + + public static void tick() { + TrainMapRenderer map = TrainMapRenderer.INSTANCE; + if (map.trackingVersion != CreateClient.RAILWAYS.version + || map.trackingDim != Minecraft.getInstance().level.dimension() + || map.trackingTheme != AllConfigs.client().trainMapColorTheme.get()) { + redrawAll(); + } + } + + public static List renderAndPick(GuiGraphics graphics, int mouseX, int mouseY, float pt, + boolean linearFiltering, Rect2i bounds) { + Object hoveredElement = null; + + int offScreenMargin = 32; + bounds.setX(bounds.getX() - offScreenMargin); + bounds.setY(bounds.getY() - offScreenMargin); + bounds.setWidth(bounds.getWidth() + 2 * offScreenMargin); + bounds.setHeight(bounds.getHeight() + 2 * offScreenMargin); + + TrainMapRenderer.INSTANCE.render(graphics, mouseX, mouseY, pt, linearFiltering, bounds); + hoveredElement = drawTrains(graphics, mouseX, mouseY, pt, hoveredElement, bounds); + hoveredElement = drawPoints(graphics, mouseX, mouseY, pt, hoveredElement, bounds); + + graphics.bufferSource() + .endBatch(); + + if (hoveredElement instanceof GlobalStation station) + return List.of(Components.literal(station.name)); + + if (hoveredElement instanceof Train train) + return listTrainDetails(train); + + return null; + } + + public static void renderToggleWidget(GuiGraphics graphics, int x, int y) { + boolean enabled = AllConfigs.client().showTrainMapOverlay.get(); + if (CreateClient.RAILWAYS.trackNetworks.isEmpty()) + return; + RenderSystem.enableBlend(); + PoseStack pose = graphics.pose(); + pose.pushPose(); + pose.translate(0, 0, 300); + AllGuiTextures.TRAINMAP_TOGGLE_PANEL.render(graphics, x, y); + (enabled ? AllGuiTextures.TRAINMAP_TOGGLE_ON : AllGuiTextures.TRAINMAP_TOGGLE_OFF).render(graphics, x + 18, + y + 3); + pose.popPose(); + } + + public static boolean handleToggleWidgetClick(int mouseX, int mouseY, int x, int y) { + if (!isToggleWidgetHovered(mouseX, mouseY, x, y)) + return false; + CClient config = AllConfigs.client(); + config.showTrainMapOverlay.set(!config.showTrainMapOverlay.get()); + return true; + } + + public static boolean isToggleWidgetHovered(int mouseX, int mouseY, int x, int y) { + if (CreateClient.RAILWAYS.trackNetworks.isEmpty()) + return false; + if (mouseX < x || mouseX >= x + AllGuiTextures.TRAINMAP_TOGGLE_PANEL.width) + return false; + if (mouseY < y || mouseY >= y + AllGuiTextures.TRAINMAP_TOGGLE_PANEL.height) + return false; + return true; + } + + private static List listTrainDetails(Train train) { + List output = new ArrayList<>(); + int blue = 0xD3DEDC; + int darkBlue = 0x92A9BD; + int bright = 0xFFEFEF; + int orange = 0xFFAD60; + + TrainMapSyncEntry trainEntry = TrainMapSyncClient.currentData.get(train.id); + if (trainEntry == null) + return Collections.emptyList(); + TrainState state = trainEntry.state; + SignalState signalState = trainEntry.signalState; + + Lang.text(train.name.getString()) + .color(bright) + .addTo(output); + + if (!trainEntry.ownerName.isBlank()) + Lang.translate("train_map.train_owned_by", trainEntry.ownerName) + .color(blue) + .addTo(output); + + switch (state) { + + case CONDUCTOR_MISSING: + Lang.translate("train_map.conductor_missing") + .color(orange) + .addTo(output); + return output; + case DERAILED: + Lang.translate("train_map.derailed") + .color(orange) + .addTo(output); + return output; + case NAVIGATION_FAILED: + Lang.translate("train_map.navigation_failed") + .color(orange) + .addTo(output); + return output; + case SCHEDULE_INTERRUPTED: + Lang.translate("train_map.schedule_interrupted") + .color(orange) + .addTo(output); + return output; + case RUNNING_MANUALLY: + Lang.translate("train_map.player_controlled") + .color(blue) + .addTo(output); + break; + + case RUNNING: + default: + break; + } + + String currentStation = trainEntry.targetStationName; + int targetStationDistance = trainEntry.targetStationDistance; + + if (!currentStation.isBlank()) { + if (targetStationDistance == 0) + Lang.translate("train_map.train_at_station", currentStation) + .color(darkBlue) + .addTo(output); + else + Lang.translate("train_map.train_moving_to_station", currentStation, targetStationDistance) + .color(darkBlue) + .addTo(output); + } + + if (signalState != SignalState.NOT_WAITING) { + boolean chainSignal = signalState == SignalState.CHAIN_SIGNAL; + Lang.translate("train_map.waiting_at_signal") + .color(orange) + .addTo(output); + + if (signalState == SignalState.WAITING_FOR_REDSTONE) + Lang.translate("train_map.redstone_powered") + .color(blue) + .addTo(output); + else { + UUID waitingFor = trainEntry.waitingForTrain; + boolean trainFound = false; + + if (waitingFor != null) { + Train trainWaitingFor = CreateClient.RAILWAYS.trains.get(waitingFor); + if (trainWaitingFor != null) { + Lang.translate("train_map.for_other_train", trainWaitingFor.name.getString()) + .color(blue) + .addTo(output); + trainFound = true; + } + } + + if (!trainFound) { + if (chainSignal) + Lang.translate("train_map.cannot_traverse_section") + .color(blue) + .addTo(output); + else + Lang.translate("train_map.section_reserved") + .color(blue) + .addTo(output); + } + } + } + + if (trainEntry.fueled) + Lang.translate("train_map.fuel_boosted") + .color(darkBlue) + .addTo(output); + + return output; + } + + private static Object drawPoints(GuiGraphics graphics, int mouseX, int mouseY, float pt, Object hoveredElement, + Rect2i bounds) { + PoseStack pose = graphics.pose(); + RenderSystem.enableDepthTest(); + + for (TrackGraph graph : CreateClient.RAILWAYS.trackNetworks.values()) { + for (GlobalStation station : graph.getPoints(EdgePointType.STATION)) { + + Couple edgeLocation = station.edgeLocation; + TrackNode node = graph.locateNode(edgeLocation.getFirst()); + TrackNode other = graph.locateNode(edgeLocation.getSecond()); + if (node == null || other == null) + continue; + if (node.getLocation().dimension != TrainMapRenderer.INSTANCE.trackingDim) + continue; + + TrackEdge edge = graph.getConnection(Couple.create(node, other)); + if (edge == null) + continue; + + double tLength = station.getLocationOn(edge); + double t = tLength / edge.getLength(); + Vec3 position = edge.getPosition(graph, t); + + int x = Mth.floor(position.x()); + int y = Mth.floor(position.z()); + + if (!bounds.contains(x, y)) + continue; + + Vec3 diff = edge.getDirectionAt(tLength) + .normalize(); + int rotation = Mth.positiveModulo(Mth.floor(0.5 + + (Math.atan2(diff.z, diff.x) * Mth.RAD_TO_DEG + 90 + (station.isPrimary(node) ? 180 : 0)) / 45), + 8); + + AllGuiTextures sprite = AllGuiTextures.TRAINMAP_STATION_ORTHO; + AllGuiTextures highlightSprite = AllGuiTextures.TRAINMAP_STATION_ORTHO_HIGHLIGHT; + if (rotation % 2 != 0) { + sprite = AllGuiTextures.TRAINMAP_STATION_DIAGO; + highlightSprite = AllGuiTextures.TRAINMAP_STATION_DIAGO_HIGHLIGHT; + } + + boolean highlight = hoveredElement == null && Math.max(Math.abs(mouseX - x), Math.abs(mouseY - y)) < 3; + + pose.pushPose(); + pose.translate(x - 2, y - 2, 5); + + pose.translate(sprite.width / 2.0, sprite.height / 2.0, 0); + pose.mulPose(Axis.ZP.rotationDegrees(90 * (rotation / 2))); + pose.translate(-sprite.width / 2.0, -sprite.height / 2.0, 0); + + sprite.render(graphics, 0, 0); + sprite.render(graphics, 0, 0); + + if (highlight) { + pose.translate(0, 0, 5); + highlightSprite.render(graphics, -1, -1); + hoveredElement = station; + } + + pose.popPose(); + } + } + + return hoveredElement; + } + + private static Object drawTrains(GuiGraphics graphics, int mouseX, int mouseY, float pt, Object hoveredElement, + Rect2i bounds) { + PoseStack pose = graphics.pose(); + RenderSystem.enableDepthTest(); + RenderSystem.enableBlend(); + + int spriteYOffset = -3; + + double time = AnimationTickHolder.getTicks(); + time += AnimationTickHolder.getPartialTicks(); + time -= TrainMapSyncClient.lastPacket; + time /= TrainMapSync.lightPacketInterval; + time = Mth.clamp(time, 0, 1); + + int[] sliceXShiftByRotationIndex = new int[] { 0, 1, 2, 2, 3, -2, -2, -1 }; + int[] sliceYShiftByRotationIndex = new int[] { 3, 2, 2, 1, 0, 1, 2, 2 }; + + for (Train train : CreateClient.RAILWAYS.trains.values()) { + TrainMapSyncEntry trainEntry = TrainMapSyncClient.currentData.get(train.id); + if (trainEntry == null) + continue; + + Vec3 frontPos = Vec3.ZERO; + List carriages = train.carriages; + boolean otherDim = true; + double avgY = 0; + + for (int i = 0; i < carriages.size(); i++) { + for (boolean firstBogey : Iterate.trueAndFalse) + avgY += trainEntry.getPosition(i, firstBogey, time) + .y(); + } + + avgY /= carriages.size() * 2; + + for (int i = 0; i < carriages.size(); i++) { + Carriage carriage = carriages.get(i); + + Vec3 pos1 = trainEntry.getPosition(i, true, time); + Vec3 pos2 = trainEntry.getPosition(i, false, time); + + ResourceKey dim = trainEntry.dimensions.get(i); + if (dim == null || dim != TrainMapRenderer.INSTANCE.trackingDim) + continue; + if (!bounds.contains(Mth.floor(pos1.x()), Mth.floor(pos1.z())) + && !bounds.contains(Mth.floor(pos2.x()), Mth.floor(pos2.z()))) + continue; + + otherDim = false; + + if (!trainEntry.backwards && i == 0) + frontPos = pos1; + if (trainEntry.backwards && i == train.carriages.size() - 1) + frontPos = pos2; + + Vec3 diff = pos2.subtract(pos1); + int size = carriage.bogeySpacing + 1; + Vec3 center = pos1.add(pos2) + .scale(0.5); + + double pX = center.x; + double pY = center.z; + int rotation = + Mth.positiveModulo(Mth.floor(0.5 + (Math.atan2(diff.x, diff.z) * Mth.RAD_TO_DEG) / 22.5), 8); + + if (trainEntry.state == TrainState.DERAILED) + rotation = + Mth.positiveModulo((AnimationTickHolder.getTicks() / 8 + i * 3) * (i % 2 == 0 ? 1 : -1), 8); + + AllGuiTextures sprite = AllGuiTextures.TRAINMAP_SPRITES; + + int slices = 2; + + if (rotation == 0 || rotation == 4) { + // Orthogonal, slices add 3 pixels + slices += Mth.floor((size - 2) / (3.0) + 0.5); + } + + else if (rotation == 2 || rotation == 6) { + // Diagonal, slices add 2*sqrt(2) pixels + slices += Mth.floor((size - (5 - 2 * Mth.SQRT_OF_TWO)) / (2 * Mth.SQRT_OF_TWO) + 0.5); + } + + else { + // Slanty, slices add sqrt(5) pixels + slices += Mth.floor((size - (5 - Mth.sqrt(5))) / (Mth.sqrt(5)) + 0.5); + } + + slices = Math.max(2, slices); + + sprite.bind(); + pose.pushPose(); + + float pivotX = 7.5f + (slices - 3) * sliceXShiftByRotationIndex[rotation] / 2.0f; + float pivotY = 6.5f + (slices - 3) * sliceYShiftByRotationIndex[rotation] / 2.0f; + // Ysort at home + pose.translate(pX - pivotX, pY - pivotY, 10 + (avgY / 512.0) + (1024.0 + center.z() % 8192.0) / 1024.0); + + int trainColorIndex = train.mapColorIndex; + int colorRow = trainColorIndex / 4; + int colorCol = trainColorIndex % 4; + + for (int slice = 0; slice < slices; slice++) { + int row = slice == 0 ? 1 : slice == slices - 1 ? 2 : 3; + int sliceShifts = slice == 0 ? 0 : slice == slices - 1 ? slice - 2 : slice - 1; + int col = rotation; + + int positionX = sliceShifts * sliceXShiftByRotationIndex[rotation]; + int positionY = sliceShifts * sliceYShiftByRotationIndex[rotation] + spriteYOffset; + int sheetX = col * 16 + colorCol * 128; + int sheetY = row * 16 + colorRow * 64; + + graphics.blit(sprite.location, positionX, positionY, sheetX, sheetY, 16, 16, sprite.width, + sprite.height); + } + + pose.popPose(); + + int margin = 1; + int sizeX = 8 + (slices - 3) * sliceXShiftByRotationIndex[rotation]; + int sizeY = 12 + (slices - 3) * sliceYShiftByRotationIndex[rotation]; + double pXm = pX - sizeX / 2; + double pYm = pY - sizeY / 2 + spriteYOffset; + if (hoveredElement == null && mouseX < pXm + margin + sizeX && mouseX > pXm - margin + && mouseY < pYm + margin + sizeY && mouseY > pYm - margin) + hoveredElement = train; + } + + if (otherDim) + continue; + + if (trainEntry.signalState != SignalState.NOT_WAITING) { + pose.pushPose(); + pose.translate(frontPos.x - 0.5, frontPos.z - 0.5, 20 + (1024.0 + frontPos.z() % 8192.0) / 1024.0); + AllGuiTextures.TRAINMAP_SIGNAL.render(graphics, 0, -3); + pose.popPose(); + } + } + + return hoveredElement; + } + + // Background first so we can mindlessly paint over it + static final int PHASE_BACKGROUND = 0; + // Straights before curves so that curves anti-alias properly at the transition + static final int PHASE_STRAIGHTS = 1; + static final int PHASE_CURVES = 2; + + public static void redrawAll() { + TrainMapRenderer map = TrainMapRenderer.INSTANCE; + map.trackingVersion = CreateClient.RAILWAYS.version; + map.trackingDim = Minecraft.getInstance().level.dimension(); + map.trackingTheme = AllConfigs.client().trainMapColorTheme.get(); + map.startDrawing(); + + int mainColor = 0xFF_7C57D4; + int darkerColor = 0xFF_70437D; + int darkerColorShadow = 0xFF_4A2754; + + switch (map.trackingTheme) { + case GREY: + mainColor = 0xFF_A8B5B5; + darkerColor = 0xFF_776E6C; + darkerColorShadow = 0xFF_56504E; + break; + case WHITE: + mainColor = 0xFF_E8F9F9; + darkerColor = 0xFF_889595; + darkerColorShadow = 0xFF_56504E; + break; + default: + break; + } + + List> collisions = new ObjectArrayList<>(); + + for (int phase = 0; phase <= 2; phase++) + renderPhase(map, collisions, mainColor, darkerColor, phase); + + highlightYDifferences(map, collisions, mainColor, darkerColor, darkerColor, darkerColorShadow); + + map.finishDrawing(); + } + + private static void renderPhase(TrainMapRenderer map, List> collisions, int mainColor, + int darkerColor, int phase) { + int outlineColor = 0xFF_000000; + + int portalFrameColor = 0xFF_4C2D5B; + int portalColor = 0xFF_FF7FD6; + + for (TrackGraph graph : CreateClient.RAILWAYS.trackNetworks.values()) { + for (TrackNodeLocation nodeLocation : graph.getNodes()) { + if (nodeLocation.dimension != map.trackingDim) + continue; + TrackNode node = graph.locateNode(nodeLocation); + Map connectionsFrom = graph.getConnectionsFrom(node); + + int hashCode = node.hashCode(); + for (Entry entry : connectionsFrom.entrySet()) { + TrackNode other = entry.getKey(); + TrackNodeLocation otherLocation = other.getLocation(); + TrackEdge edge = entry.getValue(); + BezierConnection turn = edge.getTurn(); + + // Portal track + if (edge.isInterDimensional()) { + Vec3 vec = node.getLocation() + .getLocation(); + int x = Mth.floor(vec.x); + int z = Mth.floor(vec.z); + if (phase == PHASE_CURVES) + continue; + if (phase == PHASE_BACKGROUND) { + map.setPixels(x - 3, z - 2, x + 3, z + 2, outlineColor); + map.setPixels(x - 2, z - 3, x + 2, z + 3, outlineColor); + continue; + } + + int a = mapYtoAlpha(Mth.floor(vec.y())); + for (int xi = x - 2; xi <= x + 2; xi++) { + for (int zi = z - 2; zi <= z + 2; zi++) { + int alphaAt = map.alphaAt(xi, zi); + if (alphaAt > 0 && alphaAt != a) + collisions.add(Couple.create(xi, zi)); + int c = (xi - x) * (xi - x) + (zi - z) * (zi - z) > 2 ? portalFrameColor : portalColor; + if (alphaAt <= a) { + map.setPixel(xi, zi, markY(c, vec.y())); + } + } + } + continue; + } + + if (other.hashCode() > hashCode) + continue; + + if (turn == null) { + if (phase == PHASE_CURVES) + continue; + + float x1 = nodeLocation.getX(); + float z1 = nodeLocation.getZ(); + float x2 = otherLocation.getX(); + float z2 = otherLocation.getZ(); + + double y1 = nodeLocation.getLocation() + .y(); + double y2 = otherLocation.getLocation() + .y(); + + float xDiffSign = Math.signum(x2 - x1); + float zDiffSign = Math.signum(z2 - z1); + boolean diagonal = xDiffSign != 0 && zDiffSign != 0; + + if (xDiffSign != 0) { + x2 -= xDiffSign * .25; + x1 += xDiffSign * .25; + } + + if (zDiffSign != 0) { + z2 -= zDiffSign * .25; + z1 += zDiffSign * .25; + } + + x1 /= 2; + x2 /= 2; + z1 /= 2; + z2 /= 2; + + int y = Mth.floor(y1); + int a = mapYtoAlpha(y); + + // Diagonal + if (diagonal) { + int z = Mth.floor(z1); + int x = Mth.floor(x1); + + for (int s = 0; s <= Math.abs(x1 - x2); s++) { + if (phase == PHASE_BACKGROUND) { + map.setPixels(x - 1, z, x + 1, z + 1, outlineColor); + map.setPixels(x, z - 1, x, z + 2, outlineColor); + x += xDiffSign; + z += zDiffSign; + continue; + } + + int alphaAt = map.alphaAt(x, z); + if (alphaAt > 0 && alphaAt != a) + collisions.add(Couple.create(x, z)); + if (alphaAt <= a) { + map.setPixel(x, z, markY(mainColor, y)); + } + + if (map.alphaAt(x, z + 1) < a) { + map.setPixel(x, z + 1, markY(darkerColor, y)); + } + + x += xDiffSign; + z += zDiffSign; + } + + continue; + } + + // Straight + if (phase == PHASE_BACKGROUND) { + int x1i = Mth.floor(Math.min(x1, x2)); + int z1i = Mth.floor(Math.min(z1, z2)); + int x2i = Mth.floor(Math.max(x1, x2)); + int z2i = Mth.floor(Math.max(z1, z2)); + + map.setPixels(x1i - 1, z1i, x2i + 1, z2i, outlineColor); + map.setPixels(x1i, z1i - 1, x2i, z2i + 1, outlineColor); + continue; + } + + int z = Mth.floor(z1); + int x = Mth.floor(x1); + float diff = Math.max(Math.abs(x1 - x2), Math.abs(z1 - z2)); + double yStep = (y2 - y1) / diff; + + for (int s = 0; s <= diff; s++) { + int alphaAt = map.alphaAt(x, z); + if (alphaAt > 0 && alphaAt != a) + collisions.add(Couple.create(x, z)); + if (alphaAt <= a) { + map.setPixel(x, z, markY(mainColor, y)); + } + x += xDiffSign; + y += yStep; + z += zDiffSign; + } + + continue; + } + + if (phase == PHASE_STRAIGHTS) + continue; + + BlockPos origin = turn.tePositions.getFirst(); + Map, Double> rasterise = turn.rasterise(); + + for (boolean antialias : Iterate.falseAndTrue) { + for (Entry, Double> offset : rasterise.entrySet()) { + Pair xz = offset.getKey(); + int x = origin.getX() + xz.getFirst(); + int y = Mth.floor(origin.getY() + offset.getValue() + 0.5); + int z = origin.getZ() + xz.getSecond(); + + if (phase == PHASE_BACKGROUND) { + map.setPixels(x - 1, z, x + 1, z, outlineColor); + map.setPixels(x, z - 1, x, z + 1, outlineColor); + continue; + } + + int a = mapYtoAlpha(y); + + if (!antialias) { + int alphaAt = map.alphaAt(x, z); + if (alphaAt > 0 && alphaAt != a) + collisions.add(Couple.create(x, z)); + if (alphaAt > a) + continue; + + map.setPixel(x, z, markY(mainColor, y)); + continue; + } + + boolean mainColorBelowLeft = + map.is(x + 1, z + 1, mainColor) && Math.abs(map.alphaAt(x + 1, z + 1) - a) <= 1; + boolean mainColorBelowRight = + map.is(x - 1, z + 1, mainColor) && Math.abs(map.alphaAt(x - 1, z + 1) - a) <= 1; + + if (mainColorBelowLeft || mainColorBelowRight) { + int alphaAt = map.alphaAt(x, z + 1); + if (alphaAt > 0 && alphaAt != a) + collisions.add(Couple.create(x, z)); + if (alphaAt >= a) + continue; + + map.setPixel(x, z + 1, markY(darkerColor, y)); + + // Adjust background + if (map.isEmpty(x + 1, z + 1)) + map.setPixel(x + 1, z + 1, outlineColor); + if (map.isEmpty(x - 1, z + 1)) + map.setPixel(x - 1, z + 1, outlineColor); + if (map.isEmpty(x, z + 2)) + map.setPixel(x, z + 2, outlineColor); + } + } + if (phase == PHASE_BACKGROUND) + break; + } + } + } + } + } + + private static void highlightYDifferences(TrainMapRenderer map, List> collisions, int mainColor, + int darkerColor, int mainColorShadow, int darkerColorShadow) { + for (Couple couple : collisions) { + int x = couple.getFirst(); + int z = couple.getSecond(); + int a = map.alphaAt(x, z); + if (a == 0) + continue; + + for (int xi = x - 2; xi <= x + 2; xi++) { + for (int zi = z - 2; zi <= z + 2; zi++) { + if (map.alphaAt(xi, zi) >= a) + continue; + if (map.is(xi, zi, mainColor)) + map.setPixel(xi, zi, FastColor.ABGR32.color(a, mainColorShadow)); + else if (map.is(xi, zi, darkerColor)) + map.setPixel(xi, zi, FastColor.ABGR32.color(a, darkerColorShadow)); + } + } + } + } + + private static int mapYtoAlpha(double y) { + int minY = Minecraft.getInstance().level.getMinBuildHeight(); + return Mth.clamp(32 + Mth.floor((y - minY) / 4.0), 0, 255); + } + + private static int markY(int color, double y) { + return FastColor.ABGR32.color(mapYtoAlpha(y), color); + } + +} diff --git a/src/main/java/com/simibubi/create/compat/trainmap/TrainMapRenderer.java b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapRenderer.java new file mode 100644 index 000000000..e5335bb4d --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapRenderer.java @@ -0,0 +1,259 @@ +package com.simibubi.create.compat.trainmap; + +import java.util.HashSet; +import java.util.Set; + +import org.joml.Matrix4f; + +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.simibubi.create.foundation.render.RenderTypes; +import com.simibubi.create.foundation.utility.Couple; +import com.simibubi.create.infrastructure.config.CClient; + +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.MultiBufferSource.BufferSource; +import net.minecraft.client.renderer.Rect2i; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; + +public class TrainMapRenderer implements AutoCloseable { + + public static final TrainMapRenderer INSTANCE = new TrainMapRenderer(); + public static final int WIDTH = 128, HEIGHT = 128; + private Object2ObjectMap, TrainMapInstance> maps = new Object2ObjectOpenHashMap<>(); + + public int trackingVersion; + public ResourceKey trackingDim; + public CClient.TrainMapTheme trackingTheme; + + // + + private TrainMapInstance previouslyAccessed; + + public void startDrawing() { + previouslyAccessed = null; + maps.values() + .forEach(tmi -> { + tmi.getImage() + .fillRect(0, 0, WIDTH, HEIGHT, 0); + tmi.untouched = true; + }); + } + + public Object2ObjectMap, TrainMapInstance> getMaps() { + return maps; + } + + public void setPixel(int xCoord, int zCoord, int color) { + TrainMapInstance instance = getOrCreateAt(xCoord, zCoord); + xCoord = Mth.positiveModulo(xCoord, WIDTH); + zCoord = Mth.positiveModulo(zCoord, HEIGHT); + instance.getImage() + .setPixelRGBA(xCoord, zCoord, color); + } + + public int getPixel(int xCoord, int zCoord) { + Couple sectionKey = toSectionKey(xCoord, zCoord); + if (!maps.containsKey(sectionKey)) + return 0; + + TrainMapInstance instance = getOrCreateAt(xCoord, zCoord); + xCoord = Mth.positiveModulo(xCoord, WIDTH); + zCoord = Mth.positiveModulo(zCoord, HEIGHT); + return instance.getImage() + .getPixelRGBA(xCoord, zCoord); + } + + public void setPixels(int xCoordFrom, int zCoordFrom, int xCoordTo, int zCoordTo, int color) { + for (int x = Math.min(xCoordFrom, xCoordTo); x <= Math.max(xCoordFrom, xCoordTo); x++) + for (int z = Math.min(zCoordFrom, zCoordTo); z <= Math.max(zCoordFrom, zCoordTo); z++) + setPixel(x, z, color); + } + + public void blendPixel(int xCoord, int zCoord, int color, int alpha) { + TrainMapInstance instance = getOrCreateAt(xCoord, zCoord); + xCoord = Mth.positiveModulo(xCoord, WIDTH); + zCoord = Mth.positiveModulo(zCoord, HEIGHT); + instance.getImage() + .blendPixel(xCoord, zCoord, FastColor.ABGR32.color(alpha, color)); + } + + public void blendPixels(int xCoordFrom, int zCoordFrom, int xCoordTo, int zCoordTo, int color, int alpha) { + for (int x = Math.min(xCoordFrom, xCoordTo); x <= Math.max(xCoordFrom, xCoordTo); x++) + for (int z = Math.min(zCoordFrom, zCoordTo); z <= Math.max(zCoordFrom, zCoordTo); z++) + blendPixel(x, z, color, alpha); + } + + public void finishDrawing() { + previouslyAccessed = null; + Set> stale = new HashSet<>(); + + maps.forEach((key, tmi) -> { + if (!tmi.untouched) + return; + tmi.close(); + stale.add(key); + }); + + stale.forEach(key -> { + TrainMapInstance tmi = maps.remove(key); + if (tmi != null) + tmi.close(); + }); + } + + public boolean is(int x, int z, int color) { + return (getPixel(x, z) & 0xFFFFFF) == (color & 0xFFFFFF); + } + + public boolean isEmpty(int x, int z) { + return getPixel(x, z) == 0; + } + + public int alphaAt(int x, int z) { + int pixel = getPixel(x, z); + return ((pixel & 0xFFFFFF) != 0) ? ((pixel >>> 24) & 0xFF) : 0; + } + + // + + public void render(GuiGraphics graphics, int mouseX, int mouseY, float pt, boolean linearFiltering, Rect2i bounds) { + BufferSource bufferSource = graphics.bufferSource(); + PoseStack pose = graphics.pose(); + maps.forEach((key, tmi) -> { + if (tmi.canBeSkipped(bounds)) + return; + int x = key.getFirst(); + int y = key.getSecond(); + pose.pushPose(); + pose.translate(x * WIDTH, y * HEIGHT, 0); + tmi.draw(pose, bufferSource, linearFiltering); + pose.popPose(); + }); + } + + public TrainMapInstance getOrCreateAt(int xCoord, int zCoord) { + Couple sectionKey = toSectionKey(xCoord, zCoord); + if (previouslyAccessed != null && previouslyAccessed.sectionKey.equals(sectionKey)) + return previouslyAccessed; + return maps.compute(sectionKey, (key, instance) -> instance == null ? new TrainMapInstance(key) : instance); + } + + public Couple toSectionKey(int xCoord, int zCoord) { + return Couple.create(Mth.floor(xCoord / (float) WIDTH), Mth.floor(zCoord / (float) HEIGHT)); + } + + public void resetData() { + for (TrainMapInstance instance : maps.values()) + instance.close(); + maps.clear(); + } + + public void close() { + this.resetData(); + } + + public class TrainMapInstance implements AutoCloseable { + + private DynamicTexture texture; + private RenderType renderType; + private boolean requiresUpload; + private boolean linearFiltering; + private Rect2i bounds; + + private boolean untouched; + private Couple sectionKey; + + public ResourceLocation location; + + public TrainMapInstance(Couple sectionKey) { + TextureManager textureManager = Minecraft.getInstance() + .getTextureManager(); + + this.sectionKey = sectionKey; + untouched = false; + requiresUpload = true; + texture = new DynamicTexture(128, 128, true); + linearFiltering = false; + location = textureManager + .register("create_trainmap/" + sectionKey.getFirst() + "_" + sectionKey.getSecond(), texture); + renderType = RenderTypes.TRAIN_MAP.apply(location, linearFiltering); + bounds = new Rect2i(sectionKey.getFirst() * WIDTH, sectionKey.getSecond() * HEIGHT, WIDTH, HEIGHT); + } + + public boolean canBeSkipped(Rect2i bounds) { + return bounds.getX() + bounds.getWidth() < this.bounds.getX() + || this.bounds.getX() + this.bounds.getWidth() < bounds.getX() + || bounds.getY() + bounds.getHeight() < this.bounds.getY() + || this.bounds.getY() + this.bounds.getHeight() < bounds.getY(); + } + + public NativeImage getImage() { + untouched = false; + requiresUpload = true; + return texture.getPixels(); + } + + public void draw(PoseStack pPoseStack, MultiBufferSource pBufferSource, boolean linearFiltering) { + if (texture.getPixels() == null) + return; + + if (requiresUpload) { + texture.upload(); + requiresUpload = false; + } + + if (pPoseStack == null) + return; + + if (linearFiltering != this.linearFiltering) { + this.linearFiltering = linearFiltering; + renderType = RenderTypes.TRAIN_MAP.apply(location, linearFiltering); + } + + int pPackedLight = LightTexture.FULL_BRIGHT; + + Matrix4f matrix4f = pPoseStack.last() + .pose(); + VertexConsumer vertexconsumer = pBufferSource.getBuffer(renderType); + vertexconsumer.vertex(matrix4f, 0.0F, HEIGHT, 0) + .color(255, 255, 255, 255) + .uv(0.0F, 1.0F) + .uv2(pPackedLight) + .endVertex(); + vertexconsumer.vertex(matrix4f, WIDTH, HEIGHT, 0) + .color(255, 255, 255, 255) + .uv(1.0F, 1.0F) + .uv2(pPackedLight) + .endVertex(); + vertexconsumer.vertex(matrix4f, WIDTH, 0.0F, 0) + .color(255, 255, 255, 255) + .uv(1.0F, 0.0F) + .uv2(pPackedLight) + .endVertex(); + vertexconsumer.vertex(matrix4f, 0.0F, 0.0F, 0) + .color(255, 255, 255, 255) + .uv(0.0F, 0.0F) + .uv2(pPackedLight) + .endVertex(); + } + + public void close() { + texture.close(); + } + + } +} diff --git a/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSync.java b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSync.java new file mode 100644 index 000000000..201caa0b4 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSync.java @@ -0,0 +1,344 @@ +package com.simibubi.create.compat.trainmap; + +import java.lang.ref.WeakReference; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.simibubi.create.AllPackets; +import com.simibubi.create.Create; +import com.simibubi.create.content.trains.entity.Carriage; +import com.simibubi.create.content.trains.entity.Carriage.DimensionalCarriageEntity; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.entity.TravellingPoint; +import com.simibubi.create.content.trains.graph.DimensionPalette; +import com.simibubi.create.content.trains.graph.EdgePointType; +import com.simibubi.create.content.trains.schedule.ScheduleRuntime; +import com.simibubi.create.content.trains.signal.SignalBlock.SignalType; +import com.simibubi.create.content.trains.signal.SignalBoundary; +import com.simibubi.create.content.trains.signal.SignalEdgeGroup; +import com.simibubi.create.content.trains.station.GlobalStation; +import com.simibubi.create.foundation.utility.Pair; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.event.TickEvent.ServerTickEvent; +import net.minecraftforge.network.PacketDistributor; + +public class TrainMapSync { + + public static final int lightPacketInterval = 5; + public static final int fullPacketInterval = 10; + + public static int ticks; + + public enum TrainState { + RUNNING, RUNNING_MANUALLY, DERAILED, SCHEDULE_INTERRUPTED, CONDUCTOR_MISSING, NAVIGATION_FAILED + } + + public enum SignalState { + NOT_WAITING, WAITING_FOR_REDSTONE, BLOCK_SIGNAL, CHAIN_SIGNAL + } + + public static class TrainMapSyncEntry { + + // Clientside + public float[] prevPositions; + + // Updated every 5 ticks + public float[] positions; + public List> dimensions; + public TrainState state = TrainState.RUNNING; + public SignalState signalState = SignalState.NOT_WAITING; + public boolean fueled = false; + public boolean backwards = false; + public int targetStationDistance = 0; + + // Updated every 10 ticks + public String ownerName = ""; + public String targetStationName = ""; + public UUID waitingForTrain = null; + + public void gatherDimensions(DimensionPalette dimensionPalette) { + for (ResourceKey resourceKey : dimensions) + if (resourceKey != null) + dimensionPalette.encode(resourceKey); + } + + public void send(FriendlyByteBuf buffer, DimensionPalette dimensionPalette, boolean light) { + buffer.writeVarInt(positions.length); + for (float f : positions) + buffer.writeFloat(f); + + buffer.writeVarInt(dimensions.size()); + for (ResourceKey resourceKey : dimensions) + buffer.writeVarInt(resourceKey == null ? -1 : dimensionPalette.encode(resourceKey)); + + buffer.writeVarInt(state.ordinal()); + buffer.writeVarInt(signalState.ordinal()); + buffer.writeBoolean(fueled); + buffer.writeBoolean(backwards); + buffer.writeVarInt(targetStationDistance); + + if (light) + return; + + buffer.writeUtf(ownerName); + buffer.writeUtf(targetStationName); + + buffer.writeBoolean(waitingForTrain != null); + if (waitingForTrain != null) + buffer.writeUUID(waitingForTrain); + } + + public void receive(FriendlyByteBuf buffer, DimensionPalette dimensionPalette, boolean light) { + positions = new float[buffer.readVarInt()]; + for (int i = 0; i < positions.length; i++) + positions[i] = buffer.readFloat(); + + dimensions = new ArrayList<>(); + int dimensionsSize = buffer.readVarInt(); + for (int i = 0; i < dimensionsSize; i++) { + int index = buffer.readVarInt(); + dimensions.add(index == -1 ? null : dimensionPalette.decode(index)); + } + + state = TrainState.values()[buffer.readVarInt()]; + signalState = SignalState.values()[buffer.readVarInt()]; + fueled = buffer.readBoolean(); + backwards = buffer.readBoolean(); + targetStationDistance = buffer.readVarInt(); + + if (light) + return; + + ownerName = buffer.readUtf(); + targetStationName = buffer.readUtf(); + + waitingForTrain = null; + if (buffer.readBoolean()) + waitingForTrain = buffer.readUUID(); + } + + public void updateFrom(TrainMapSyncEntry other, boolean light) { + prevPositions = positions; + + positions = other.positions; + state = other.state; + signalState = other.signalState; + fueled = other.fueled; + backwards = other.backwards; + targetStationDistance = other.targetStationDistance; + + if (light) + return; + + ownerName = other.ownerName; + targetStationName = other.targetStationName; + waitingForTrain = other.waitingForTrain; + } + + public Vec3 getPosition(int carriageIndex, boolean firstBogey, double time) { + int startIndex = carriageIndex * 6 + (firstBogey ? 0 : 3); + if (positions == null || positions.length <= startIndex + 2) + return Vec3.ZERO; + Vec3 position = new Vec3(positions[startIndex], positions[startIndex + 1], positions[startIndex + 2]); + if (prevPositions == null || prevPositions.length <= startIndex + 2) + return position; + Vec3 prevPosition = + new Vec3(prevPositions[startIndex], prevPositions[startIndex + 1], prevPositions[startIndex + 2]); + return prevPosition.lerp(position, time); + } + + } + + public static Cache> requestingPlayers = CacheBuilder.newBuilder() + .expireAfterWrite(Duration.ofSeconds(1)) + .build(); + + public static void requestReceived(ServerPlayer sender) { + boolean sendImmediately = requestingPlayers.getIfPresent(sender.getUUID()) == null; + requestingPlayers.put(sender.getUUID(), new WeakReference<>(sender)); + if (sendImmediately) + send(sender.server, false); + } + + public static void serverTick(ServerTickEvent event) { + ticks++; + if (ticks % fullPacketInterval == 0) + send(event.getServer(), false); + else if (ticks % lightPacketInterval == 0) + send(event.getServer(), true); + } + + public static void send(MinecraftServer minecraftServer, boolean light) { + if (requestingPlayers.size() == 0) + return; + + TrainMapSyncPacket packet = new TrainMapSyncPacket(light); + for (Train train : Create.RAILWAYS.trains.values()) + packet.add(train.id, createEntry(minecraftServer, train)); + + for (WeakReference weakReference : requestingPlayers.asMap() + .values()) { + ServerPlayer player = weakReference.get(); + if (player == null) + continue; + AllPackets.getChannel() + .send(PacketDistributor.PLAYER.with(() -> player), packet); + } + } + + private static TrainMapSyncEntry createEntry(MinecraftServer minecraftServer, Train train) { + TrainMapSyncEntry entry = new TrainMapSyncEntry(); + boolean stopped = Math.abs(train.speed) < 0.05; + + entry.positions = new float[train.carriages.size() * 6]; + entry.dimensions = new ArrayList<>(); + + List carriages = train.carriages; + for (int i = 0; i < carriages.size(); i++) { + Carriage carriage = carriages.get(i); + Vec3 leadingPos; + Vec3 trailingPos; + + if (train.graph == null) { + + // Train is derailed + Pair, DimensionalCarriageEntity> dimCarriage = + carriage.anyAvailableDimensionalCarriage(); + if (dimCarriage == null || carriage.presentInMultipleDimensions()) { + entry.dimensions.add(null); + continue; + } + + leadingPos = dimCarriage.getSecond().rotationAnchors.getFirst(); + trailingPos = dimCarriage.getSecond().rotationAnchors.getSecond(); + + if (leadingPos == null || trailingPos == null) { + entry.dimensions.add(null); + continue; + } + + entry.dimensions.add(dimCarriage.getFirst()); + + } else { + + // Train is on Track + TravellingPoint leading = carriage.getLeadingPoint(); + TravellingPoint trailing = carriage.getTrailingPoint(); + if (leading == null || trailing == null || leading.edge == null || trailing.edge == null) { + entry.dimensions.add(null); + continue; + } + + ResourceKey leadingDim = + (leading.node1 == null || leading.edge == null || leading.edge.isInterDimensional()) ? null + : leading.node1.getLocation() + .getDimension(); + + ResourceKey trailingDim = + (trailing.node1 == null || trailing.edge == null || trailing.edge.isInterDimensional()) ? null + : trailing.node1.getLocation() + .getDimension(); + + ResourceKey carriageDim = (leadingDim == null || leadingDim != trailingDim) ? null : leadingDim; + entry.dimensions.add(carriageDim); + + leadingPos = leading.getPosition(train.graph); + trailingPos = trailing.getPosition(train.graph); + } + + entry.positions[i * 6] = (float) leadingPos.x(); + entry.positions[i * 6 + 1] = (float) leadingPos.y(); + entry.positions[i * 6 + 2] = (float) leadingPos.z(); + + entry.positions[i * 6 + 3] = (float) trailingPos.x(); + entry.positions[i * 6 + 4] = (float) trailingPos.y(); + entry.positions[i * 6 + 5] = (float) trailingPos.z(); + } + + entry.backwards = train.currentlyBackwards; + + if (train.owner != null) { + ServerPlayer owner = minecraftServer.getPlayerList() + .getPlayer(train.owner); + if (owner != null) + entry.ownerName = owner.getName() + .getString(); + } + + if (train.derailed) { + entry.state = TrainState.DERAILED; + return entry; + } + + ScheduleRuntime runtime = train.runtime; + if (runtime.getSchedule() != null && stopped) { + if (runtime.paused) { + entry.state = TrainState.SCHEDULE_INTERRUPTED; + return entry; + } + + if (train.status.conductor) { + entry.state = TrainState.CONDUCTOR_MISSING; + return entry; + } + + if (train.status.navigation) { + entry.state = TrainState.NAVIGATION_FAILED; + return entry; + } + } + + if ((runtime.getSchedule() == null || runtime.paused) && train.speed != 0) + entry.state = TrainState.RUNNING_MANUALLY; + + GlobalStation currentStation = train.getCurrentStation(); + if (currentStation != null) { + entry.targetStationName = currentStation.name; + entry.targetStationDistance = 0; + } else if (train.navigation.destination != null && !runtime.paused) { + entry.targetStationName = train.navigation.destination.name; + entry.targetStationDistance = Math.max(0, Mth.floor(train.navigation.distanceToDestination)); + } + + if (stopped && train.navigation.waitingForSignal != null) { + UUID signalId = train.navigation.waitingForSignal.getFirst(); + boolean side = train.navigation.waitingForSignal.getSecond(); + SignalBoundary signal = train.graph.getPoint(EdgePointType.SIGNAL, signalId); + + if (signal != null) { + boolean chainSignal = signal.types.get(side) == SignalType.CROSS_SIGNAL; + entry.signalState = chainSignal ? SignalState.CHAIN_SIGNAL : SignalState.BLOCK_SIGNAL; + if (signal.isForcedRed(side)) + entry.signalState = SignalState.WAITING_FOR_REDSTONE; + else { + SignalEdgeGroup group = Create.RAILWAYS.signalEdgeGroups.get(signal.groups.get(side)); + if (group != null) { + for (Train other : group.trains) { + if (other == train) + continue; + entry.waitingForTrain = other.id; + break; + } + } + } + } + } + + if (train.fuelTicks > 0 && !stopped) + entry.fueled = true; + + return entry; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncClient.java b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncClient.java new file mode 100644 index 000000000..321b40fc4 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncClient.java @@ -0,0 +1,56 @@ +package com.simibubi.create.compat.trainmap; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.simibubi.create.AllPackets; +import com.simibubi.create.compat.trainmap.TrainMapSync.TrainMapSyncEntry; +import com.simibubi.create.foundation.utility.AnimationTickHolder; +import com.simibubi.create.foundation.utility.Pair; + +public class TrainMapSyncClient { + + public static Map currentData = new HashMap<>(); + + public static double lastPacket; + + private static int ticks; + + public static void requestData() { + ticks++; + if (ticks % 5 == 0) + AllPackets.getChannel() + .sendToServer(new TrainMapSyncRequestPacket()); + } + + public static void stopRequesting() { + ticks = 0; + currentData.clear(); + } + + public static void receive(TrainMapSyncPacket packet) { + if (ticks == 0) + return; + + lastPacket = AnimationTickHolder.getTicks(); + lastPacket += AnimationTickHolder.getPartialTicks(); + + Set staleEntries = new HashSet<>(); + staleEntries.addAll(currentData.keySet()); + + for (Pair pair : packet.entries) { + UUID id = pair.getFirst(); + TrainMapSyncEntry entry = pair.getSecond(); + staleEntries.remove(id); + currentData.computeIfAbsent(id, $ -> entry) + .updateFrom(entry, packet.light); + } + + for (UUID uuid : staleEntries) + currentData.remove(uuid); + } + +} diff --git a/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncPacket.java b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncPacket.java new file mode 100644 index 000000000..0c3703759 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncPacket.java @@ -0,0 +1,65 @@ +package com.simibubi.create.compat.trainmap; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.simibubi.create.compat.trainmap.TrainMapSync.TrainMapSyncEntry; +import com.simibubi.create.content.trains.graph.DimensionPalette; +import com.simibubi.create.foundation.networking.SimplePacketBase; +import com.simibubi.create.foundation.utility.Pair; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent.Context; + +public class TrainMapSyncPacket extends SimplePacketBase { + + public List> entries = new ArrayList<>(); + public boolean light; + + public TrainMapSyncPacket(boolean light) { + this.light = light; + } + + public void add(UUID trainId, TrainMapSyncEntry data) { + entries.add(Pair.of(trainId, data)); + } + + public TrainMapSyncPacket(FriendlyByteBuf buffer) { + DimensionPalette dimensionPalette = DimensionPalette.receive(buffer); + light = buffer.readBoolean(); + + int size = buffer.readVarInt(); + for (int i = 0; i < size; i++) { + UUID id = buffer.readUUID(); + TrainMapSyncEntry entry = new TrainMapSyncEntry(); + entry.receive(buffer, dimensionPalette, light); + entries.add(Pair.of(id, entry)); + } + } + + @Override + public void write(FriendlyByteBuf buffer) { + DimensionPalette dimensionPalette = new DimensionPalette(); + for (Pair pair : entries) + pair.getSecond() + .gatherDimensions(dimensionPalette); + + dimensionPalette.send(buffer); + buffer.writeBoolean(light); + + buffer.writeVarInt(entries.size()); + for (Pair pair : entries) { + buffer.writeUUID(pair.getFirst()); + pair.getSecond() + .send(buffer, dimensionPalette, light); + } + } + + @Override + public boolean handle(Context context) { + context.enqueueWork(() -> TrainMapSyncClient.receive(this)); + return true; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncRequestPacket.java b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncRequestPacket.java new file mode 100644 index 000000000..c6138a56d --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncRequestPacket.java @@ -0,0 +1,23 @@ +package com.simibubi.create.compat.trainmap; + +import com.simibubi.create.foundation.networking.SimplePacketBase; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent.Context; + +public class TrainMapSyncRequestPacket extends SimplePacketBase { + + public TrainMapSyncRequestPacket() {} + + public TrainMapSyncRequestPacket(FriendlyByteBuf buffer) {} + + @Override + public void write(FriendlyByteBuf buffer) {} + + @Override + public boolean handle(Context context) { + context.enqueueWork(() -> TrainMapSync.requestReceived(context.getSender())); + return true; + } + +} diff --git a/src/main/java/com/simibubi/create/content/trains/GlobalRailwayManager.java b/src/main/java/com/simibubi/create/content/trains/GlobalRailwayManager.java index 73d9d244a..25b944d78 100644 --- a/src/main/java/com/simibubi/create/content/trains/GlobalRailwayManager.java +++ b/src/main/java/com/simibubi/create/content/trains/GlobalRailwayManager.java @@ -48,6 +48,8 @@ public class GlobalRailwayManager { private List waitingTrains; private RailwaySavedData savedData; + + public int version; public GlobalRailwayManager() { cleanUp(); diff --git a/src/main/java/com/simibubi/create/content/trains/entity/Carriage.java b/src/main/java/com/simibubi/create/content/trains/entity/Carriage.java index b340e4848..19883c1be 100644 --- a/src/main/java/com/simibubi/create/content/trains/entity/Carriage.java +++ b/src/main/java/com/simibubi/create/content/trains/entity/Carriage.java @@ -29,6 +29,7 @@ import com.simibubi.create.foundation.advancement.AllAdvancements; import com.simibubi.create.foundation.utility.Couple; import com.simibubi.create.foundation.utility.Iterate; import com.simibubi.create.foundation.utility.NBTHelper; +import com.simibubi.create.foundation.utility.Pair; import com.simibubi.create.foundation.utility.VecHelper; import net.minecraft.core.BlockPos; @@ -422,6 +423,13 @@ public class Carriage { return null; } + public Pair, DimensionalCarriageEntity> anyAvailableDimensionalCarriage() { + for (Entry, DimensionalCarriageEntity> entry : entities.entrySet()) + if (entry.getValue().entity.get() != null) + return Pair.of(entry.getKey(), entry.getValue()); + return null; + } + public void forEachPresentEntity(Consumer callback) { for (DimensionalCarriageEntity dimensionalCarriageEntity : entities.values()) { CarriageContraptionEntity entity = dimensionalCarriageEntity.entity.get(); diff --git a/src/main/java/com/simibubi/create/content/trains/entity/Train.java b/src/main/java/com/simibubi/create/content/trains/entity/Train.java index c12913e18..0b0927144 100644 --- a/src/main/java/com/simibubi/create/content/trains/entity/Train.java +++ b/src/main/java/com/simibubi/create/content/trains/entity/Train.java @@ -95,6 +95,7 @@ public class Train { public Navigation navigation; public ScheduleRuntime runtime; public TrainIconType icon; + public int mapColorIndex; public Component name; public TrainStatus status; @@ -932,6 +933,8 @@ public class Train { @Nullable public LivingEntity getOwner(Level level) { + if (level.getServer() == null) + return null; try { UUID uuid = owner; return uuid == null ? null @@ -1130,6 +1133,7 @@ public class Train { tag.putInt("Fuel", fuelTicks); tag.putDouble("TargetSpeed", targetSpeed); tag.putString("IconType", icon.id.toString()); + tag.putInt("MapColorIndex", mapColorIndex); tag.putString("Name", Component.Serializer.toJson(name)); if (currentStation != null) tag.putUUID("Station", currentStation); @@ -1182,6 +1186,7 @@ public class Train { train.speedBeforeStall = tag.getDouble("SpeedBeforeStall"); train.targetSpeed = tag.getDouble("TargetSpeed"); train.icon = TrainIconType.byId(new ResourceLocation(tag.getString("IconType"))); + train.mapColorIndex = tag.getInt("MapColorIndex"); train.name = Component.Serializer.fromJson(tag.getString("Name")); train.currentStation = tag.contains("Station") ? tag.getUUID("Station") : null; train.currentlyBackwards = tag.getBoolean("Backwards"); diff --git a/src/main/java/com/simibubi/create/content/trains/entity/TrainPacket.java b/src/main/java/com/simibubi/create/content/trains/entity/TrainPacket.java index 288555ea4..1598c5379 100644 --- a/src/main/java/com/simibubi/create/content/trains/entity/TrainPacket.java +++ b/src/main/java/com/simibubi/create/content/trains/entity/TrainPacket.java @@ -68,6 +68,7 @@ public class TrainPacket extends SimplePacketBase { train.name = Component.Serializer.fromJson(buffer.readUtf()); train.icon = TrainIconType.byId(buffer.readResourceLocation()); + train.mapColorIndex = buffer.readVarInt(); } @Override @@ -105,6 +106,7 @@ public class TrainPacket extends SimplePacketBase { buffer.writeBoolean(train.doubleEnded); buffer.writeUtf(Component.Serializer.toJson(train.name)); buffer.writeResourceLocation(train.icon.id); + buffer.writeVarInt(train.mapColorIndex); } @Override diff --git a/src/main/java/com/simibubi/create/content/trains/entity/TrainStatus.java b/src/main/java/com/simibubi/create/content/trains/entity/TrainStatus.java index 1a3e73d84..8a68292a9 100644 --- a/src/main/java/com/simibubi/create/content/trains/entity/TrainStatus.java +++ b/src/main/java/com/simibubi/create/content/trains/entity/TrainStatus.java @@ -16,9 +16,9 @@ public class TrainStatus { Train train; - boolean navigation; - boolean track; - boolean conductor; + public boolean navigation; + public boolean track; + public boolean conductor; List queued = new ArrayList<>(); diff --git a/src/main/java/com/simibubi/create/content/trains/graph/TrackGraphSyncPacket.java b/src/main/java/com/simibubi/create/content/trains/graph/TrackGraphSyncPacket.java index e5cda7b42..f3b04b13f 100644 --- a/src/main/java/com/simibubi/create/content/trains/graph/TrackGraphSyncPacket.java +++ b/src/main/java/com/simibubi/create/content/trains/graph/TrackGraphSyncPacket.java @@ -171,6 +171,8 @@ public class TrackGraphSyncPacket extends TrackGraphPacket { @Override protected void handle(GlobalRailwayManager manager, TrackGraph graph) { + manager.version++; + if (packetDeletesGraph) { manager.removeGraph(graph); return; diff --git a/src/main/java/com/simibubi/create/content/trains/station/AssemblyScreen.java b/src/main/java/com/simibubi/create/content/trains/station/AssemblyScreen.java index 28cdceeef..947649d35 100644 --- a/src/main/java/com/simibubi/create/content/trains/station/AssemblyScreen.java +++ b/src/main/java/com/simibubi/create/content/trains/station/AssemblyScreen.java @@ -49,7 +49,7 @@ public class AssemblyScreen extends AbstractStationScreen { iconTypes = TrainIconType.REGISTRY.keySet() .stream() .toList(); - iconTypeScroll = new ScrollInput(x + 4, y + 17, 184, 14).titled(Lang.translateDirect("station.icon_type")); + iconTypeScroll = new ScrollInput(x + 4, y + 17, 162, 14).titled(Lang.translateDirect("station.icon_type")); iconTypeScroll.withRange(0, iconTypes.size()); iconTypeScroll.withStepFunction(ctx -> -iconTypeScroll.standardStep() .apply(ctx)); @@ -164,7 +164,7 @@ public class AssemblyScreen extends AbstractStationScreen { ResourceLocation iconId = iconTypes.get(iconTypeScroll.getState()); train.icon = TrainIconType.byId(iconId); AllPackets.getChannel() - .sendToServer(new TrainEditPacket(train.id, "", iconId)); + .sendToServer(new TrainEditPacket(train.id, "", iconId, train.mapColorIndex)); } } diff --git a/src/main/java/com/simibubi/create/content/trains/station/StationBlockEntity.java b/src/main/java/com/simibubi/create/content/trains/station/StationBlockEntity.java index 7c76d6050..0e6979f9d 100644 --- a/src/main/java/com/simibubi/create/content/trains/station/StationBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/trains/station/StationBlockEntity.java @@ -467,6 +467,18 @@ public class StationBlockEntity extends SmartBlockEntity implements ITransformab itemEntity.setDeltaMovement(Vec3.ZERO); getLevel().addFreshEntity(itemEntity); } + + public void updateMapColor(int color) { + GlobalStation station = getStation(); + if (station == null) + return; + + Train train = station.getPresentTrain(); + if (train == null) + return; + + train.mapColorIndex = color; + } private boolean updateStationState(Consumer updateState) { GlobalStation station = getStation(); diff --git a/src/main/java/com/simibubi/create/content/trains/station/StationScreen.java b/src/main/java/com/simibubi/create/content/trains/station/StationScreen.java index ffbdb584f..548e4de4b 100644 --- a/src/main/java/com/simibubi/create/content/trains/station/StationScreen.java +++ b/src/main/java/com/simibubi/create/content/trains/station/StationScreen.java @@ -11,6 +11,7 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.AllBlocks; import com.simibubi.create.AllPackets; import com.simibubi.create.AllPartialModels; +import com.simibubi.create.compat.Mods; import com.simibubi.create.content.decoration.slidingDoor.DoorControl; import com.simibubi.create.content.trains.entity.Carriage; import com.simibubi.create.content.trains.entity.Train; @@ -21,6 +22,7 @@ import com.simibubi.create.foundation.gui.UIRenderHelper; import com.simibubi.create.foundation.gui.widget.IconButton; import com.simibubi.create.foundation.gui.widget.Label; import com.simibubi.create.foundation.gui.widget.ScrollInput; +import com.simibubi.create.foundation.utility.AnimationTickHolder; import com.simibubi.create.foundation.utility.Components; import com.simibubi.create.foundation.utility.Lang; import com.simibubi.create.foundation.utility.Pair; @@ -44,6 +46,9 @@ public class StationScreen extends AbstractStationScreen { private int leavingAnimation; private LerpedFloat trainPosition; private DoorControl doorControl; + + private ScrollInput colorTypeScroll; + private int messedWithColors; private boolean switchingToAssemblyMode; @@ -99,6 +104,20 @@ public class StationScreen extends AbstractStationScreen { dropScheduleButton.withCallback(() -> AllPackets.getChannel() .sendToServer(StationEditPacket.dropSchedule(blockEntity.getBlockPos()))); addRenderableWidget(dropScheduleButton); + + colorTypeScroll = new ScrollInput(x + 166, y + 17, 22, 14).titled(Lang.translateDirect("station.train_map_color")); + colorTypeScroll.withRange(0, 16); + colorTypeScroll.withStepFunction(ctx -> -colorTypeScroll.standardStep() + .apply(ctx)); + colorTypeScroll.calling(s -> { + Train train = displayedTrain.get(); + if (train != null) { + train.mapColorIndex = s; + messedWithColors = 10; + } + }); + colorTypeScroll.active = colorTypeScroll.visible = false; + addRenderableWidget(colorTypeScroll); onTextChanged = s -> trainNameBox.setX(nameBoxX(s, trainNameBox)); trainNameBox = new EditBox(font, x + 23, y + 47, background.width - 75, 10, Components.immutableEmpty()); @@ -131,6 +150,12 @@ public class StationScreen extends AbstractStationScreen { .length()); trainNameBox.setHighlightPos(trainNameBox.getCursorPosition()); } + + if (messedWithColors > 0) { + messedWithColors--; + if (messedWithColors == 0) + syncTrainNameAndColor(); + } super.tick(); @@ -151,6 +176,8 @@ public class StationScreen extends AbstractStationScreen { leavingAnimation = 0; newTrainButton.active = blockEntity.edgePoint.isOrthogonal(); newTrainButton.visible = true; + colorTypeScroll.visible = false; + colorTypeScroll.active = false; Train imminentTrain = getImminent(); if (imminentTrain != null) { @@ -161,7 +188,9 @@ public class StationScreen extends AbstractStationScreen { disassembleTrainButton.visible = true; dropScheduleButton.active = blockEntity.trainHasSchedule; dropScheduleButton.visible = true; - + colorTypeScroll.setState(imminentTrain.mapColorIndex); + colorTypeScroll.visible = true; + colorTypeScroll.active = true; trainNameBox.active = true; trainNameBox.setValue(imminentTrain.name.getString()); trainNameBox.setX(nameBoxX(trainNameBox.getValue(), trainNameBox)); @@ -185,6 +214,8 @@ public class StationScreen extends AbstractStationScreen { targetPos -= trainIconWidth - 130; if (leavingAnimation > 0) { + colorTypeScroll.visible = false; + colorTypeScroll.active = false; disassembleTrainButton.active = false; float f = 1 - (leavingAnimation / 80f); trainPosition.setValue(targetPos + f * f * f * (background.width - targetPos + 5)); @@ -301,6 +332,27 @@ public class StationScreen extends AbstractStationScreen { if (font.width(text) > trainNameBox.getWidth()) graphics.drawString(font, "...", guiLeft + 26, guiTop + 47, 0xa6a6a6); } + + if (!Mods.FTBCHUNKS.isLoaded()) + return; + + AllGuiTextures sprite = AllGuiTextures.TRAINMAP_SPRITES; + sprite.bind(); + int trainColorIndex = colorTypeScroll.getState(); + int colorRow = trainColorIndex / 4; + int colorCol = trainColorIndex % 4; + int rotation = (AnimationTickHolder.getTicks() / 5) % 8; + + for (int slice = 0; slice < 3; slice++) { + int row = slice == 0 ? 1 : slice == 2 ? 2 : 3; + int col = rotation; + int positionX = colorTypeScroll.getX() + 4; + int positionY = colorTypeScroll.getY() - 1; + int sheetX = col * 16 + colorCol * 128; + int sheetY = row * 16 + colorRow * 64; + + graphics.blit(sprite.location, positionX, positionY, sheetX, sheetY, 16, 16, sprite.width, sprite.height); + } } @Override @@ -335,19 +387,19 @@ public class StationScreen extends AbstractStationScreen { if (hitEnter && trainNameBox.isFocused()) { trainNameBox.setFocused(false); - syncTrainName(); + syncTrainNameAndColor(); return true; } return super.keyPressed(pKeyCode, pScanCode, pModifiers); } - private void syncTrainName() { + private void syncTrainNameAndColor() { Train train = displayedTrain.get(); if (train != null && !trainNameBox.getValue() .equals(train.name.getString())) AllPackets.getChannel() - .sendToServer(new TrainEditPacket(train.id, trainNameBox.getValue(), train.icon.getId())); + .sendToServer(new TrainEditPacket(train.id, trainNameBox.getValue(), train.icon.getId(), train.mapColorIndex)); } private void syncStationName() { @@ -371,7 +423,8 @@ public class StationScreen extends AbstractStationScreen { return; if (!switchingToAssemblyMode) AllPackets.getChannel() - .sendToServer(new TrainEditPacket(train.id, trainNameBox.getValue(), train.icon.getId())); + .sendToServer( + new TrainEditPacket(train.id, trainNameBox.getValue(), train.icon.getId(), train.mapColorIndex)); else blockEntity.imminentTrain = null; } diff --git a/src/main/java/com/simibubi/create/content/trains/station/TrainEditPacket.java b/src/main/java/com/simibubi/create/content/trains/station/TrainEditPacket.java index 5ee103bef..d2ba96eab 100644 --- a/src/main/java/com/simibubi/create/content/trains/station/TrainEditPacket.java +++ b/src/main/java/com/simibubi/create/content/trains/station/TrainEditPacket.java @@ -21,17 +21,20 @@ public class TrainEditPacket extends SimplePacketBase { private String name; private UUID id; private ResourceLocation iconType; + private int mapColor; - public TrainEditPacket(UUID id, String name, ResourceLocation iconType) { + public TrainEditPacket(UUID id, String name, ResourceLocation iconType, int mapColor) { this.name = name; this.id = id; this.iconType = iconType; + this.mapColor = mapColor; } public TrainEditPacket(FriendlyByteBuf buffer) { id = buffer.readUUID(); name = buffer.readUtf(256); iconType = buffer.readResourceLocation(); + mapColor = buffer.readVarInt(); } @Override @@ -39,6 +42,7 @@ public class TrainEditPacket extends SimplePacketBase { buffer.writeUUID(id); buffer.writeUtf(name); buffer.writeResourceLocation(iconType); + buffer.writeVarInt(mapColor); } @Override @@ -52,8 +56,9 @@ public class TrainEditPacket extends SimplePacketBase { if (!name.isBlank()) train.name = Components.literal(name); train.icon = TrainIconType.byId(iconType); + train.mapColorIndex = mapColor; if (sender != null) - AllPackets.getChannel().send(PacketDistributor.ALL.noArg(), new TrainEditReturnPacket(id, name, iconType)); + AllPackets.getChannel().send(PacketDistributor.ALL.noArg(), new TrainEditReturnPacket(id, name, iconType, mapColor)); }); return true; } @@ -64,8 +69,8 @@ public class TrainEditPacket extends SimplePacketBase { super(buffer); } - public TrainEditReturnPacket(UUID id, String name, ResourceLocation iconType) { - super(id, name, iconType); + public TrainEditReturnPacket(UUID id, String name, ResourceLocation iconType, int mapColor) { + super(id, name, iconType, mapColor); } } diff --git a/src/main/java/com/simibubi/create/content/trains/track/BezierConnection.java b/src/main/java/com/simibubi/create/content/trains/track/BezierConnection.java index 6f0744198..8df6b3ae0 100644 --- a/src/main/java/com/simibubi/create/content/trains/track/BezierConnection.java +++ b/src/main/java/com/simibubi/create/content/trains/track/BezierConnection.java @@ -1,6 +1,8 @@ package com.simibubi.create.content.trains.track; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import com.jozufozu.flywheel.util.transform.TransformStack; import com.mojang.blaze3d.vertex.PoseStack; @@ -9,6 +11,7 @@ import com.simibubi.create.AllBlocks; import com.simibubi.create.foundation.utility.Couple; import com.simibubi.create.foundation.utility.Iterate; import com.simibubi.create.foundation.utility.NBTHelper; +import com.simibubi.create.foundation.utility.Pair; import com.simibubi.create.foundation.utility.VecHelper; import net.minecraft.core.BlockPos; @@ -108,7 +111,8 @@ public class BezierConnection implements Iterable { .map(v -> v.add(Vec3.atLowerCornerOf(localTo))), Couple.deserializeEach(compound.getList("Axes", Tag.TAG_COMPOUND), VecHelper::readNBTCompound), Couple.deserializeEach(compound.getList("Normals", Tag.TAG_COMPOUND), VecHelper::readNBTCompound), - compound.getBoolean("Primary"), compound.getBoolean("Girder"), TrackMaterial.deserialize(compound.getString("Material"))); + compound.getBoolean("Primary"), compound.getBoolean("Girder"), + TrackMaterial.deserialize(compound.getString("Material"))); if (compound.contains("Smoothing")) smoothing = @@ -398,7 +402,8 @@ public class BezierConnection implements Iterable { } public void spawnDestroyParticles(Level level) { - BlockParticleOption data = new BlockParticleOption(ParticleTypes.BLOCK, getMaterial().getBlock().defaultBlockState()); + BlockParticleOption data = new BlockParticleOption(ParticleTypes.BLOCK, getMaterial().getBlock() + .defaultBlockState()); BlockParticleOption girderData = new BlockParticleOption(ParticleTypes.BLOCK, AllBlocks.METAL_GIRDER.getDefaultState()); if (!(level instanceof ServerLevel slevel)) @@ -673,4 +678,104 @@ public class BezierConnection implements Iterable { return bakedGirders; } + public Map, Double> rasterise() { + Map, Double> yLevels = new HashMap<>(); + BlockPos tePosition = tePositions.getFirst(); + Vec3 end1 = starts.getFirst() + .subtract(Vec3.atLowerCornerOf(tePosition)) + .add(0, 3 / 16f, 0); + Vec3 end2 = starts.getSecond() + .subtract(Vec3.atLowerCornerOf(tePosition)) + .add(0, 3 / 16f, 0); + Vec3 axis1 = axes.getFirst(); + Vec3 axis2 = axes.getSecond(); + + double handleLength = getHandleLength(); + Vec3 finish1 = axis1.scale(handleLength) + .add(end1); + Vec3 finish2 = axis2.scale(handleLength) + .add(end2); + + Vec3 faceNormal1 = normals.getFirst(); + Vec3 faceNormal2 = normals.getSecond(); + + int segCount = getSegmentCount(); + float[] lut = getStepLUT(); + Vec3[] samples = new Vec3[segCount]; + + for (int i = 0; i < segCount; i++) { + float t = Mth.clamp((i + 0.5f) * lut[i] / segCount, 0, 1); + Vec3 result = VecHelper.bezier(end1, end2, finish1, finish2, t); + Vec3 derivative = VecHelper.bezierDerivative(end1, end2, finish1, finish2, t) + .normalize(); + Vec3 faceNormal = + faceNormal1.equals(faceNormal2) ? faceNormal1 : VecHelper.slerp(t, faceNormal1, faceNormal2); + Vec3 normal = faceNormal.cross(derivative) + .normalize(); + Vec3 below = result.add(faceNormal.scale(-.25f)); + Vec3 rail1 = below.add(normal.scale(.05f)); + Vec3 rail2 = below.subtract(normal.scale(.05f)); + Vec3 railMiddle = rail1.add(rail2) + .scale(.5); + samples[i] = railMiddle; + } + + Vec3 center = end1.add(end2) + .scale(0.5); + + Pair prev = null; + Pair prev2 = null; + Pair prev3 = null; + + for (int i = 0; i < segCount; i++) { + Vec3 railMiddle = samples[i]; + BlockPos pos = BlockPos.containing(railMiddle); + Pair key = Pair.of(pos.getX(), pos.getZ()); + boolean alreadyPresent = yLevels.containsKey(key); + if (alreadyPresent && yLevels.get(key) <= railMiddle.y) + continue; + yLevels.put(key, railMiddle.y); + if (alreadyPresent) + continue; + + if (prev3 != null) { // Remove obsolete pixels + boolean doubledViaPrev = isLineDoubled(prev2, prev, key); + boolean doubledViaPrev2 = isLineDoubled(prev3, prev2, prev); + boolean prevCloser = diff(prev, center) > diff(prev2, center); + + if (doubledViaPrev2 && (!doubledViaPrev || !prevCloser)) { + yLevels.remove(prev2); + prev2 = prev; + prev = key; + continue; + + } else if (doubledViaPrev && doubledViaPrev2 && prevCloser) { + yLevels.remove(prev); + prev = key; + continue; + } + } + + prev3 = prev2; + prev2 = prev; + prev = key; + } + + return yLevels; + } + + private double diff(Pair pFrom, Vec3 to) { + return to.distanceToSqr(pFrom.getFirst() + 0.5, to.y, pFrom.getSecond() + 0.5); + } + + private boolean isLineDoubled(Pair pFrom, Pair pVia, + Pair pTo) { + int diff1x = pVia.getFirst() - pFrom.getFirst(); + int diff1z = pVia.getSecond() - pFrom.getSecond(); + int diff2x = pTo.getFirst() - pVia.getFirst(); + int diff2z = pTo.getSecond() - pVia.getSecond(); + return Math.abs(diff1x) + Math.abs(diff1z) == 1 && Math.abs(diff2x) + Math.abs(diff2z) == 1 && diff1x != diff2x + && diff1z != diff2z; + } + } diff --git a/src/main/java/com/simibubi/create/content/trains/track/TrackBlockEntity.java b/src/main/java/com/simibubi/create/content/trains/track/TrackBlockEntity.java index 6cec632cb..f4ccc3aa5 100644 --- a/src/main/java/com/simibubi/create/content/trains/track/TrackBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/trains/track/TrackBlockEntity.java @@ -21,7 +21,6 @@ import com.simibubi.create.foundation.blockEntity.RemoveBlockEntityPacket; import com.simibubi.create.foundation.blockEntity.SmartBlockEntity; import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour; import com.simibubi.create.foundation.utility.Pair; -import com.simibubi.create.foundation.utility.VecHelper; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction.Axis; @@ -363,54 +362,7 @@ public class TrackBlockEntity extends SmartBlockEntity implements ITransformable } public void manageFakeTracksAlong(BezierConnection bc, boolean remove) { - Map, Double> yLevels = new HashMap<>(); - BlockPos tePosition = bc.tePositions.getFirst(); - Vec3 end1 = bc.starts.getFirst() - .subtract(Vec3.atLowerCornerOf(tePosition)) - .add(0, 3 / 16f, 0); - Vec3 end2 = bc.starts.getSecond() - .subtract(Vec3.atLowerCornerOf(tePosition)) - .add(0, 3 / 16f, 0); - Vec3 axis1 = bc.axes.getFirst(); - Vec3 axis2 = bc.axes.getSecond(); - - double handleLength = bc.getHandleLength(); - - Vec3 finish1 = axis1.scale(handleLength) - .add(end1); - Vec3 finish2 = axis2.scale(handleLength) - .add(end2); - - Vec3 faceNormal1 = bc.normals.getFirst(); - Vec3 faceNormal2 = bc.normals.getSecond(); - - int segCount = bc.getSegmentCount(); - float[] lut = bc.getStepLUT(); - - for (int i = 0; i < segCount; i++) { - float t = i == segCount ? 1 : i * lut[i] / segCount; - t += 0.5f / segCount; - - Vec3 result = VecHelper.bezier(end1, end2, finish1, finish2, t); - Vec3 derivative = VecHelper.bezierDerivative(end1, end2, finish1, finish2, t) - .normalize(); - Vec3 faceNormal = - faceNormal1.equals(faceNormal2) ? faceNormal1 : VecHelper.slerp(t, faceNormal1, faceNormal2); - Vec3 normal = faceNormal.cross(derivative) - .normalize(); - Vec3 below = result.add(faceNormal.scale(-.25f)); - Vec3 rail1 = below.add(normal.scale(.05f)); - Vec3 rail2 = below.subtract(normal.scale(.05f)); - Vec3 railMiddle = rail1.add(rail2) - .scale(.5); - - for (Vec3 vec : new Vec3[] { railMiddle }) { - BlockPos pos = BlockPos.containing(vec); - Pair key = Pair.of(pos.getX(), pos.getZ()); - if (!yLevels.containsKey(key) || yLevels.get(key) > vec.y) - yLevels.put(key, vec.y); - } - } + Map, Double> yLevels = bc.rasterise(); for (Entry, Double> entry : yLevels.entrySet()) { double yValue = entry.getValue(); @@ -419,7 +371,7 @@ public class TrackBlockEntity extends SmartBlockEntity implements ITransformable .getFirst(), floor, entry.getKey() .getSecond()); - targetPos = targetPos.offset(tePosition) + targetPos = targetPos.offset(bc.tePositions.getFirst()) .above(1); BlockState stateAtPos = level.getBlockState(targetPos); diff --git a/src/main/java/com/simibubi/create/foundation/events/CommonEvents.java b/src/main/java/com/simibubi/create/foundation/events/CommonEvents.java index 6967cace0..5ae94185e 100644 --- a/src/main/java/com/simibubi/create/foundation/events/CommonEvents.java +++ b/src/main/java/com/simibubi/create/foundation/events/CommonEvents.java @@ -1,6 +1,7 @@ package com.simibubi.create.foundation.events; import com.simibubi.create.Create; +import com.simibubi.create.compat.trainmap.TrainMapSync; import com.simibubi.create.content.contraptions.ContraptionHandler; import com.simibubi.create.content.contraptions.actors.trainControls.ControlsServerHandler; import com.simibubi.create.content.contraptions.minecart.CouplingPhysics; @@ -67,6 +68,7 @@ public class CommonEvents { Create.LAGGER.tick(); ServerSpeedProvider.serverTick(); Create.RAILWAYS.sync.serverTick(); + TrainMapSync.serverTick(event); } @SubscribeEvent diff --git a/src/main/java/com/simibubi/create/foundation/gui/AllGuiTextures.java b/src/main/java/com/simibubi/create/foundation/gui/AllGuiTextures.java index 78693e28f..06bf6274f 100644 --- a/src/main/java/com/simibubi/create/foundation/gui/AllGuiTextures.java +++ b/src/main/java/com/simibubi/create/foundation/gui/AllGuiTextures.java @@ -189,6 +189,18 @@ public enum AllGuiTextures implements ScreenElement { // PlacementIndicator PLACEMENT_INDICATOR_SHEET("placement_indicator", 0, 0, 16, 256), + + // Train Map + TRAINMAP_SPRITES("trainmap_sprite_sheet", 0, 0, 512, 256), + TRAINMAP_SIGNAL("widgets", 81, 156, 5, 10), + TRAINMAP_STATION_ORTHO("widgets", 49, 156, 5, 5), + TRAINMAP_STATION_DIAGO("widgets", 56, 156, 5, 5), + TRAINMAP_STATION_ORTHO_HIGHLIGHT("widgets", 63, 156, 7, 7), + TRAINMAP_STATION_DIAGO_HIGHLIGHT("widgets", 72, 156, 7, 7), + + TRAINMAP_TOGGLE_PANEL("widgets", 166, 74, 33, 14), + TRAINMAP_TOGGLE_ON("widgets", 166, 89, 12, 7), + TRAINMAP_TOGGLE_OFF("widgets", 166, 97, 12, 7), // ComputerCraft COMPUTER("computer", 200, 102); diff --git a/src/main/java/com/simibubi/create/foundation/mixin/CreateMixinPlugin.java b/src/main/java/com/simibubi/create/foundation/mixin/CreateMixinPlugin.java new file mode 100644 index 000000000..fb26a4534 --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/mixin/CreateMixinPlugin.java @@ -0,0 +1,43 @@ +package com.simibubi.create.foundation.mixin; + +import java.util.List; +import java.util.Set; + +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import com.simibubi.create.compat.Mods; + +public class CreateMixinPlugin implements IMixinConfigPlugin { + + @Override + public void onLoad(String mixinPackage) {} + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + if (targetClassName.equals("journeymap/client/ui/fullscreen/Fullscreen") && !Mods.JOURNEYMAP.isLoaded()) + return false; + return true; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) {} + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} + +} diff --git a/src/main/java/com/simibubi/create/foundation/mixin/compat/JourneyFullscreenMapMixin.java b/src/main/java/com/simibubi/create/foundation/mixin/compat/JourneyFullscreenMapMixin.java new file mode 100644 index 000000000..70bb99964 --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/mixin/compat/JourneyFullscreenMapMixin.java @@ -0,0 +1,40 @@ +package com.simibubi.create.foundation.mixin.compat; + +import java.awt.geom.Point2D; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.At.Shift; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.simibubi.create.compat.trainmap.JourneyTrainMap; + +import journeymap.client.render.map.GridRenderer; +import journeymap.client.ui.fullscreen.Fullscreen; +import net.minecraft.client.gui.GuiGraphics; + +@Mixin(Fullscreen.class) +public abstract class JourneyFullscreenMapMixin { + + @Shadow + private static GridRenderer gridRenderer; + + @Shadow + private Boolean isScrolling; + + @Shadow + public abstract Point2D.Double getMouseDrag(); + + @Inject(method = "Ljourneymap/client/ui/fullscreen/Fullscreen;render(Lnet/minecraft/client/gui/GuiGraphics;IIF)V", at = @At(target = "Ljourneymap/client/ui/fullscreen/Fullscreen;drawMap(Lnet/minecraft/client/gui/GuiGraphics;II)V", value = "INVOKE", shift = Shift.AFTER)) + public void create$journeyMapFullscreenRender(GuiGraphics graphics, int mouseX, int mouseY, float pt, + CallbackInfo ci) { + boolean dragging = isScrolling; + Point2D.Double mouseDrag = getMouseDrag(); + double x = gridRenderer.getCenterBlockX() - (dragging ? mouseDrag.x : 0); + double z = gridRenderer.getCenterBlockZ() - (dragging ? mouseDrag.y : 0); + JourneyTrainMap.onRender(graphics, (Fullscreen) (Object) this, x, z, mouseX, mouseY, pt); + } + +} diff --git a/src/main/java/com/simibubi/create/foundation/render/RenderTypes.java b/src/main/java/com/simibubi/create/foundation/render/RenderTypes.java index 5a18d8298..99bfba77a 100644 --- a/src/main/java/com/simibubi/create/foundation/render/RenderTypes.java +++ b/src/main/java/com/simibubi/create/foundation/render/RenderTypes.java @@ -1,12 +1,14 @@ package com.simibubi.create.foundation.render; import java.io.IOException; +import java.util.function.BiFunction; import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.VertexFormat; import com.simibubi.create.AllSpecialTextures; import com.simibubi.create.Create; +import net.minecraft.Util; import net.minecraft.client.renderer.RenderStateShard; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.ShaderInstance; @@ -125,6 +127,19 @@ public class RenderTypes extends RenderStateShard { return ITEM_PARTIAL_TRANSLUCENT; } + public static BiFunction TRAIN_MAP = Util.memoize(RenderTypes::getTrainMap); + + private static RenderType getTrainMap(ResourceLocation locationIn, boolean linearFiltering) { + RenderType.CompositeState rendertype$state = RenderType.CompositeState.builder() + .setShaderState(RENDERTYPE_TEXT_SHADER) + .setTextureState(new RenderStateShard.TextureStateShard(locationIn, linearFiltering, false)) + .setTransparencyState(NO_TRANSPARENCY) + .setLightmapState(LIGHTMAP) + .createCompositeState(false); + return RenderType.create("create_train_map", DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP, VertexFormat.Mode.QUADS, 256, + false, true, rendertype$state); + } + private static final RenderType FLUID = RenderType.create(createLayerName("fluid"), DefaultVertexFormat.NEW_ENTITY, VertexFormat.Mode.QUADS, 256, false, true, RenderType.CompositeState.builder() .setShaderState(RENDERTYPE_ENTITY_TRANSLUCENT_CULL_SHADER) diff --git a/src/main/java/com/simibubi/create/infrastructure/config/CClient.java b/src/main/java/com/simibubi/create/infrastructure/config/CClient.java index a25381f6b..fcd4af344 100644 --- a/src/main/java/com/simibubi/create/infrastructure/config/CClient.java +++ b/src/main/java/com/simibubi/create/infrastructure/config/CClient.java @@ -88,6 +88,10 @@ public class CClient extends ConfigBase { public final ConfigFloat mountedZoomMultiplier = f(3, 0, "mountedZoomMultiplier", Comments.mountedZoomMultiplier); public final ConfigBool showTrackGraphOnF3 = b(false, "showTrackGraphOnF3", Comments.showTrackGraphOnF3); public final ConfigBool showExtendedTrackGraphOnF3 = b(false, "showExtendedTrackGraphOnF3", Comments.showExtendedTrackGraphOnF3); + public final ConfigBool showTrainMapOverlay = b(true, "showTrainMapOverlay", Comments.showTrainMapOverlay); + public final ConfigBool trainMapOverlay = b(true, "showTrainMapOverlay", Comments.showTrainMapOverlay); + public final ConfigEnum trainMapColorTheme = + e(TrainMapTheme.RED, "trainMapColorTheme", Comments.trainMapColorTheme); @Override public String getName() { @@ -97,6 +101,10 @@ public class CClient extends ConfigBase { public enum PlacementIndicatorSetting { TEXTURE, TRIANGLE, NONE } + + public enum TrainMapTheme { + RED, GREY, WHITE + } private static class Comments { static String client = "Client-only settings - If you're looking for general settings, look inside your worlds serverconfig folder!"; @@ -157,6 +165,8 @@ public class CClient extends ConfigBase { static String ambientVolumeCap = "Maximum volume modifier of Ambient noise"; static String trains = "Railway related settings"; + static String showTrainMapOverlay = "Display Track Networks and Trains on supported map mods"; + static String trainMapColorTheme = "Track Network Color on maps"; static String mountedZoomMultiplier = "How far away the Camera should zoom when seated on a train"; static String showTrackGraphOnF3 = "Display nodes and edges of a Railway Network while f3 debug mode is active"; static String showExtendedTrackGraphOnF3 = "Additionally display materials of a Rail Network while f3 debug mode is active"; diff --git a/src/main/resources/assets/create/lang/default/interface.json b/src/main/resources/assets/create/lang/default/interface.json index 5214ce073..044e4d5f7 100644 --- a/src/main/resources/assets/create/lang/default/interface.json +++ b/src/main/resources/assets/create/lang/default/interface.json @@ -790,6 +790,7 @@ "create.station.cancel": "Cancel Assembly", "create.station.failed": "Assembly Failed", "create.station.icon_type": "Icon Type", + "create.station.train_map_color": "Color on Maps", "create.station.create_train": "Create new Train", "create.station.assemble_train": "Assemble Train", "create.station.disassemble_train": "Disassemble Train", @@ -810,6 +811,22 @@ "create.station.how_to_1": "Remove bogeys by breaking the block on top.", "create.station.how_to_2": "Build carriages attached to one or two bogeys each.", + "create.train_map.toggle": "Train network overlay", + "create.train_map.train_owned_by": "by %1$s", + "create.train_map.conductor_missing": " Conductor Missing", + "create.train_map.derailed": " Derailed", + "create.train_map.navigation_failed": " Navigation Failed", + "create.train_map.schedule_interrupted": " Schedule Interrupted", + "create.train_map.player_controlled": " -> Controlled by Player", + "create.train_map.train_at_station": " >| %1$s", + "create.train_map.train_moving_to_station": " >> %1$s (%2$sm)", + "create.train_map.waiting_at_signal": " Waiting at Signal", + "create.train_map.redstone_powered": " Redstone Powered", + "create.train_map.for_other_train": " for %1$s", + "create.train_map.cannot_traverse_section": " Cannot fully traverse", + "create.train_map.section_reserved": " Section reserved", + "create.train_map.fuel_boosted": " Fuel boosted \u2714", + "create.train_assembly.too_many_bogeys": "Too many Bogeys attached: %1$s", "create.train_assembly.frontmost_bogey_at_station": "Frontmost Bogey must be at Station Marker", "create.train_assembly.no_bogeys": "No Bogeys Found", diff --git a/src/main/resources/assets/create/textures/gui/trainmap_sprite_sheet.png b/src/main/resources/assets/create/textures/gui/trainmap_sprite_sheet.png new file mode 100644 index 000000000..f981e2c68 Binary files /dev/null and b/src/main/resources/assets/create/textures/gui/trainmap_sprite_sheet.png differ diff --git a/src/main/resources/assets/create/textures/gui/widgets.png b/src/main/resources/assets/create/textures/gui/widgets.png index cc8cf5bef..db162967d 100644 Binary files a/src/main/resources/assets/create/textures/gui/widgets.png and b/src/main/resources/assets/create/textures/gui/widgets.png differ diff --git a/src/main/resources/create.mixins.json b/src/main/resources/create.mixins.json index 61c014448..419f0a08f 100644 --- a/src/main/resources/create.mixins.json +++ b/src/main/resources/create.mixins.json @@ -5,6 +5,7 @@ "package": "com.simibubi.create.foundation.mixin", "compatibilityLevel": "JAVA_17", "refmap": "create.refmap.json", + "plugin": "com.simibubi.create.foundation.mixin.CreateMixinPlugin", "mixins": [ "BlockItemMixin", "ClientboundMapItemDataPacketMixin", @@ -43,7 +44,8 @@ "client.LevelRendererMixin", "client.MapRendererMapInstanceMixin", "client.PlayerRendererMixin", - "client.WindowResizeMixin" + "client.WindowResizeMixin", + "compat.JourneyFullscreenMapMixin" ], "injectors": { "defaultRequire": 1