From 7d1a3866ae0fe5b9189201b7f95685fa45d94fbf Mon Sep 17 00:00:00 2001 From: simibubi <31564874+simibubi@users.noreply.github.com> Date: Wed, 9 Oct 2024 18:25:24 +0200 Subject: [PATCH] Squashed commit of the following: commit 8ca03ded64e214b70d4edb357cd8589d96a5fc95 Author: simibubi <31564874+simibubi@users.noreply.github.com> Date: Tue Sep 3 14:10:40 2024 +0200 Hell on Overworld - Fixed incorrect dimension data in train map sync commit 86ceb9e95440e183fe3bd4392f5a760383fc089d Merge: 97205a7d5 b6856bda8 Author: simibubi <31564874+simibubi@users.noreply.github.com> Date: Tue Sep 3 12:58:34 2024 +0200 Merge branch 'mc1.20.1/dev' into mc1.20.1/train_map_integration commit 97205a7d5172be110d802b884ef4161044111211 Author: simibubi <31564874+simibubi@users.noreply.github.com> Date: Tue Sep 3 12:58:02 2024 +0200 Tube map - Added fullscreen map integration for FTB Chunks and Journeymap --- build.gradle | 24 + .../2d64935085b86659cb7857bad9701dbf9bab6e4c | 6 +- .../resources/assets/create/lang/en_ud.json | 16 + .../resources/assets/create/lang/en_us.json | 16 + .../java/com/simibubi/create/AllPackets.java | 6 +- .../java/com/simibubi/create/compat/Mods.java | 4 +- .../peripherals/StationPeripheral.java | 2 +- .../compat/trainmap/FTBChunksTrainMap.java | 158 ++++ .../compat/trainmap/JourneyTrainMap.java | 108 +++ .../compat/trainmap/TrainMapEvents.java | 56 ++ .../compat/trainmap/TrainMapManager.java | 730 ++++++++++++++++++ .../compat/trainmap/TrainMapRenderer.java | 259 +++++++ .../create/compat/trainmap/TrainMapSync.java | 353 +++++++++ .../compat/trainmap/TrainMapSyncClient.java | 56 ++ .../compat/trainmap/TrainMapSyncPacket.java | 65 ++ .../trainmap/TrainMapSyncRequestPacket.java | 23 + .../content/trains/GlobalRailwayManager.java | 2 + .../content/trains/entity/Carriage.java | 8 + .../create/content/trains/entity/Train.java | 5 + .../content/trains/entity/TrainPacket.java | 2 + .../content/trains/entity/TrainStatus.java | 6 +- .../trains/graph/TrackGraphSyncPacket.java | 2 + .../trains/station/AssemblyScreen.java | 4 +- .../trains/station/StationBlockEntity.java | 12 + .../content/trains/station/StationScreen.java | 63 +- .../trains/station/TrainEditPacket.java | 13 +- .../trains/track/BezierConnection.java | 109 ++- .../trains/track/TrackBlockEntity.java | 52 +- .../foundation/events/CommonEvents.java | 2 + .../create/foundation/gui/AllGuiTextures.java | 12 + .../foundation/mixin/CreateMixinPlugin.java | 43 ++ .../compat/JourneyFullscreenMapMixin.java | 40 + .../create/foundation/render/RenderTypes.java | 13 + .../create/infrastructure/config/CClient.java | 10 + .../assets/create/lang/default/interface.json | 17 + .../textures/gui/trainmap_sprite_sheet.png | Bin 0 -> 12190 bytes .../assets/create/textures/gui/widgets.png | Bin 9904 -> 11018 bytes src/main/resources/create.mixins.json | 4 +- 38 files changed, 2228 insertions(+), 73 deletions(-) create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/FTBChunksTrainMap.java create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/JourneyTrainMap.java create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/TrainMapEvents.java create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/TrainMapManager.java create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/TrainMapRenderer.java create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/TrainMapSync.java create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncClient.java create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncPacket.java create mode 100644 src/main/java/com/simibubi/create/compat/trainmap/TrainMapSyncRequestPacket.java create mode 100644 src/main/java/com/simibubi/create/foundation/mixin/CreateMixinPlugin.java create mode 100644 src/main/java/com/simibubi/create/foundation/mixin/compat/JourneyFullscreenMapMixin.java create mode 100644 src/main/resources/assets/create/textures/gui/trainmap_sprite_sheet.png diff --git a/build.gradle b/build.gradle index 9120046778..fb88a41c87 100644 --- a/build.gradle +++ b/build.gradle @@ -119,6 +119,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' @@ -176,6 +192,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/src/generated/resources/.cache/2d64935085b86659cb7857bad9701dbf9bab6e4c b/src/generated/resources/.cache/2d64935085b86659cb7857bad9701dbf9bab6e4c index 92eb78af19..b015606670 100644 --- a/src/generated/resources/.cache/2d64935085b86659cb7857bad9701dbf9bab6e4c +++ b/src/generated/resources/.cache/2d64935085b86659cb7857bad9701dbf9bab6e4c @@ -1,4 +1,4 @@ -// 1.20.1 2024-10-09T12:24:59.2028575 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 -d3d30a92e4f63e8acb6aa3e3358b6e8340aa8cc1 assets/create/lang/en_ud.json -a50be2f8a02b0fdd2b5a8aae9cf8df1490015707 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 ae7ae58011..3c1a15489a 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 fa90569422..1643231078 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 3c68ae47ef..4123752808 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.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 a89dce5ff8..a78d2b780d 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 c50891fe38..146742b3bf 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 final String getTrainName() throws LuaException { 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 0000000000..f60921ae47 --- /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 0000000000..74beed9a51 --- /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 0000000000..54f2adb197 --- /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 0000000000..04ddf727ac --- /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 0000000000..e5335bb4d0 --- /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 0000000000..0de7783dbe --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/trainmap/TrainMapSync.java @@ -0,0 +1,353 @@ +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; + public List> prevDims; + + // 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; + prevDims = dimensions; + + positions = other.positions; + dimensions = other.dimensions; + state = other.state; + signalState = other.signalState; + fueled = other.fueled; + backwards = other.backwards; + targetStationDistance = other.targetStationDistance; + + if (prevDims != null) + for (int i = 0; i < Math.min(prevDims.size(), dimensions.size()); i++) + if (prevDims.get(i) != dimensions.get(i)) + for (int j = 0; j < 6; j++) + prevPositions[i * 6 + j] = positions[i * 6 + j]; + + 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 0000000000..321b40fc4b --- /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 0000000000..0c37037596 --- /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 0000000000..c6138a56db --- /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 73d9d244ab..25b944d781 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 b340e48488..19883c1be8 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.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 CarriageContraptionEntity anyAvailableEntity() { 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 c12913e181..0b09271442 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 GlobalStation getCurrentStation() { @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 CompoundTag write(DimensionPalette dimensions) { 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 static Train read(CompoundTag tag, Map trackNetworks, D 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 288555ea44..1598c53790 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 TrainPacket(FriendlyByteBuf buffer) { train.name = Component.Serializer.fromJson(buffer.readUtf()); train.icon = TrainIconType.byId(buffer.readResourceLocation()); + train.mapColorIndex = buffer.readVarInt(); } @Override @@ -105,6 +106,7 @@ public void write(FriendlyByteBuf buffer) { 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 1a3e73d840..8a68292a98 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 e5cda7b42f..f3b04b13f0 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 void write(FriendlyByteBuf buffer) { @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 8fa4a0ef16..c8246bcf17 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 @@ protected void init() { 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 void removed() { 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 a620fc1511..1bcd2e0c3c 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 @@ -466,6 +466,18 @@ public void dropSchedule(@Nullable ServerPlayer sender) { 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 d990ddd126..70cd9e9989 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 @@ -10,6 +10,7 @@ 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; @@ -20,6 +21,7 @@ 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 @@ protected void init() { 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 void tick() { .length()); trainNameBox.setHighlightPos(trainNameBox.getCursorPosition()); } + + if (messedWithColors > 0) { + messedWithColors--; + if (messedWithColors == 0) + syncTrainNameAndColor(); + } super.tick(); @@ -151,6 +176,8 @@ private void tickTrainDisplay() { 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 @@ private void tickTrainDisplay() { 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 @@ private void tickTrainDisplay() { 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 @@ protected void renderWindow(GuiGraphics graphics, int mouseX, int mouseY, float 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 boolean keyPressed(int pKeyCode, int pScanCode, int pModifiers) { 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 void removed() { 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 5ee103bef2..d2ba96eab8 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 void write(FriendlyByteBuf buffer) { buffer.writeUUID(id); buffer.writeUtf(name); buffer.writeResourceLocation(iconType); + buffer.writeVarInt(mapColor); } @Override @@ -52,8 +56,9 @@ public boolean handle(Context context) { 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 TrainEditReturnPacket(FriendlyByteBuf buffer) { 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 51f59191e3..57fa0f8a13 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.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack.Pose; @@ -8,6 +10,7 @@ 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 dev.engine_room.flywheel.lib.transform.TransformStack; @@ -108,7 +111,8 @@ public BezierConnection(CompoundTag compound, BlockPos localTo) { .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 void spawnItems(Level level) { } 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 GirderAngles[] getBakedGirders() { 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 43e8a7d09c..ce0fab09d1 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 @@ -20,7 +20,6 @@ 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 dev.engine_room.flywheel.lib.visualization.VisualizationHelper; import net.minecraft.core.BlockPos; @@ -363,54 +362,7 @@ private void removeFromCurveInteractionUnsafe() { } public void manageFakeTracksAlong(BezierConnection bc, boolean remove) { - Map, Double> yLevels = new HashMap<>(); - BlockPos tePosition = bc.bePositions.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 void manageFakeTracksAlong(BezierConnection bc, boolean remove) { .getFirst(), floor, entry.getKey() .getSecond()); - targetPos = targetPos.offset(tePosition) + targetPos = targetPos.offset(bc.bePositions.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 6967cace09..5ae94185ef 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 static void onServerTick(ServerTickEvent event) { 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 78693e28fe..06bf6274fa 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 0000000000..fb26a45346 --- /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 0000000000..70bb999643 --- /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 d0acb2cc3f..0c1f711cfc 100644 --- a/src/main/java/com/simibubi/create/foundation/render/RenderTypes.java +++ b/src/main/java/com/simibubi/create/foundation/render/RenderTypes.java @@ -127,6 +127,19 @@ public static RenderType additive() { return ADDITIVE; } + 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); + } + public static RenderType fluid() { return FLUID; } 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 a25381f6b7..fcd4af3446 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 String getName() { 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 @@ private static class Comments { 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 5214ce073d..044e4d5f72 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 0000000000000000000000000000000000000000..f981e2c682573679ced7bfbfb3cd1d046a2cfaca GIT binary patch literal 12190 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 JP2VQ){{ZyyssI20 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/create/textures/gui/widgets.png b/src/main/resources/assets/create/textures/gui/widgets.png index cc8cf5bef63005aae0a1a6dc73e7910080d3cf8e..db162967dc90e28ab12c9184f50dafc4612734db 100644 GIT binary patch literal 11018 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 literal 9904 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